├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile-py2.7 ├── LICENSE ├── QtPyConvert_CLA_Corporate.md ├── QtPyConvert_CLA_Individual.md ├── README.md ├── _resources └── logo.gif ├── requirements.txt ├── src ├── bin │ ├── qresource_convert │ └── qt_py_convert └── python │ └── qt_py_convert │ ├── __init__.py │ ├── _modules │ ├── __init__.py │ ├── expand_stars │ │ ├── __init__.py │ │ └── process.py │ ├── from_imports │ │ ├── __init__.py │ │ └── process.py │ ├── imports │ │ ├── __init__.py │ │ └── process.py │ ├── psep0101 │ │ ├── __init__.py │ │ ├── _c_args.py │ │ ├── _conversion_methods.py │ │ ├── _qsignal.py │ │ └── process.py │ └── unsupported │ │ ├── __init__.py │ │ └── process.py │ ├── color.py │ ├── diff.py │ ├── general.py │ ├── log.py │ ├── mappings.py │ └── run.py └── tests ├── test_core ├── __init__.py ├── test_binding_supported.py └── test_replacements.py ├── test_psep0101 ├── __init__.py ├── test_qsignal.py └── test_qvariant.py └── test_qtcompat ├── __init__.py └── test_compatibility_members.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: required 4 | dist: trusty 5 | 6 | python: 7 | - 2.7 8 | # - 3.5 9 | 10 | services: 11 | - docker 12 | 13 | install: 14 | - docker build -f Dockerfile-py${TRAVIS_PYTHON_VERSION} -t digitaldomain/qt_py_convert . 15 | 16 | # Enable virtual framebuffer, this way we can instantiate 17 | # a QApplication and create widgets. 18 | # https://docs.travis-ci.com/user/gui-and-headless-browsers/ 19 | before_script: 20 | - "export DISPLAY=:99.0" 21 | - "sh -e /etc/init.d/xvfb start" 22 | - sleep 3 # give xvfb some time to start 23 | 24 | script: 25 | - docker run --rm -v $(pwd):/QtPyConvert digitaldomain/qt_py_convert 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to `qt_py_convert` are documented in this file. 3 | 4 | The `qt_py_convert` package adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | 7 | 23 | 24 | 25 | ## [Unreleased] 26 | 27 | [Nothing yet] 28 | 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to QtPyConvert 2 | 3 | Thanks for taking the time to contribute! 4 | 5 | In here you'll find a series of guidelines for how you can make QtPyConvert better suit your needs and the needs of the target audience - film, games and tv. 6 | 7 | QtPyConvert was born to help companies with large legacy codebases convert their code in a timely manner and also to help the industry standardize on [Qt.py](https://github.com.mottosso/Qt.py). 8 | 9 | **Table of contents** 10 | 11 | - [Development goals](#development-goals) 12 | - [Converting Binding Support](#converting-binding-support) 13 | - [Incompatibility Warnings](#incompatibility-warnings) 14 | - [Keep it simple](#keep-it-simple) 15 | - [Normalize Imports](#normalize-imports) 16 | - [How can I contribute?](#how-can-i-contribute) 17 | - [Reporting Bugs](#reporting-bugs) 18 | - [Suggesting Enhancements](#suggesting-enhancements) 19 | - [Your First Code Contribution](#your-first-code-contribution) 20 | - [Pull Requests](#pull-requests) 21 | - [Style](#style) 22 | - [Commits](#commits) 23 | - [Version bumping](#version-bumping) 24 | - [Making a release](#making-a-release) 25 | 26 |
27 | 28 | ### Development Goals 29 | 30 | QtPyConvert aims to help in the conversion of old Qt for Python code by automating the large majority of the work for you. It does this efficiently by leaving much of the work up to the Qt.py developers and using the resulting abstraction layer as a guideline and framework. 31 | 32 | 33 | Convert any code using any of the four major Qt for Python bindings into the standardized [Qt.py abstraction layer](https://github.com/mottosso/Qt.py). 34 | 35 | Warn users about incompatibilities or unsupported code. (WIP) 36 | 37 | Standardize Qt imports to maintain sanity in code comprehension. 38 | > Removing start imports and deep class/module imports 39 | 40 | 41 | | Goal | Description 42 | |:---------------------------|:--------------- 43 | | [*Convert code from any of the four major bindings.*](#converting-binding-support) | We should support everything that Qt.py does. 44 | | [*Warn about incompatibilities.*](#incompatibility-warnings) | If code cannot be converted or functionality is unsupported in Qt.py, we should warn. 45 | | [*Keep it simple*](#keep-it-simple) | Limit the heavy lifting, PEP008. 46 | | [*Normalize import format*](#normalize-imports) | Imports should be *sane* and normalized.. 47 | 48 | Each of these deserve some explanation and rationale. 49 | 50 |
51 | 52 | ##### Converting Binding Support 53 | 54 | Running QtPyConvert should work on any source file, even if it doesn't use any Qt code. It should also have first party support for any bindings that Qt.py supports. Additional support for custom bindings either developed inhouse or online see pycode.qt, should have a way to be defined either through environment variables or a supplimentary site package that we look for. 55 | 56 |
57 | 58 | ##### Incompatibility Warnings 59 | 60 | Several patterns are unsupported using Qt.py, these include but are not limited to **QVariants**, **QStrings**, and other Api-1.0 code. These should either be automatically converted, or at the very least, printed out as a warning that the user can look into themselves. 61 | Ideally, it would be good to let users know about deprecated Qt code and provide a flag to attempt converting this as well. 62 | 63 | ##### Keep it simple 64 | 65 | QtPyConvert is mainly a conversion wrapper around Qt.py. 66 | It tries to read what Qt.py is doing by looking at it's private values. 67 | 68 | Ideally we don't want QtPyConvert doing much conversion logic related to the actual mapping of methods and classes. 69 | Instead, we want to pay attention to api1.0 to api2.0 problems, Qt4 to Qt5 deprecation problems and to a certain extent, the Qt.py QtCompat changes. 70 | 71 |
72 | 73 | ##### Normalize Imports 74 | 75 | One of the design decisions that was made early on was to normalize all Qt imports. 76 | This was partially due to preference and partially to step around the complications that would arise from keeping all of the deep level imports. 77 | 78 | ```python 79 | # *Wrong* 80 | from PyQt4.QtCore import Qt 81 | from PyQt4.QtGui import QCheckBox as chk 82 | from PyQt4.QtCore.Qt import Unchecked, Checked 83 | 84 | def click(state): 85 | print("Foo!") 86 | 87 | c = chk() 88 | c.clicked.stateChanged(click, Qt.QueuedConnection) 89 | c.setCheckState(UnChecked) 90 | ``` 91 | ```python 92 | # *Right* 93 | from Qt import QtCore, QtWidgets 94 | 95 | def click(state): 96 | print("Foo!") 97 | 98 | c = QtWidgets.QCheckBox() 99 | c.clicked.stateChanged(click, QtCore.Qt.QueuedConnection) 100 | c.setCheckState(QtCore.Qt.UnChecked) 101 | ``` 102 | 103 | This is one of the most notable *opinions* that QtPyConvert will enforce upon your code. 104 | Another notable one that it will enforce is shown below 105 | ```python 106 | from PyQt4.QtGui import * 107 | from PyQt4.QtCore import * 108 | from PyQt4.QtCore.Qt import * 109 | 110 | app = QApplication([]) 111 | app.setLayoutDirection(RightToLeft) 112 | widget = QWidget() 113 | widget.show() 114 | app.exec_() 115 | ``` 116 | 117 | ```python 118 | from Qt import QtWidgets, QtCore 119 | 120 | app = QtWidgets.QApplication([]) 121 | app.setLayoutDirection(QtCore.Qt.RightToLeft) 122 | widget = QtWidgets.QWidget() 123 | widget.show() 124 | app.exec_() 125 | ``` 126 | 127 | It is bad practice to use star imports and unless you tell it not to, QtPyConvert will resolve your imports and pare them down. 128 | 129 |
130 | 131 | ## How can I contribute? 132 | 133 | Contribution comes in many flavors, some of which is simply notifying us of problems or successes, so we know what to change and not to change. 134 | 135 | ### Contributor License Agreement 136 | 137 | Before contributing code to QtPyConvert, we ask that you sign a Contributor License Agreement (CLA). At the root of the repo you can find the two possible CLAs: 138 | 139 | - QtPyConvert_CLA_Corporate.md: please sign this one for corporate use 140 | - QtPyConvert_CLA_Individual.md: please sign this one if you're an individual contributor 141 | 142 | Once your CLA is signed, send it to opensource@d2.com (please make sure to include your github username) and wait for confirmation that we've received it. After that, you can submit pull requests. 143 | 144 | 145 | ### Reporting bugs 146 | 147 | Bug reports must include: 148 | 149 | 1. Description 150 | 2. Expected results 151 | 3. Short reproducible 152 | 153 | ### Suggesting enhancements 154 | 155 | Feature requests must include: 156 | 157 | 1. Goal (what the feature aims to solve) 158 | 2. Motivation (why *you* think this is necessary) 159 | 3. Suggested implementation (pseudocode) 160 | 161 | Questions may also be submitted as issues. 162 | 163 | ### Pull requests 164 | 165 | Code contributions are made by (1) forking this project and (2) making a modification to it. Ideally we would prefer it preceded by an issue where we discuss the feature or problem on a conceptual level before attempting an implementation of it. 166 | 167 | This is where we perform code review - where we take a moment to look through and discuss potential design decisions made towards the goal you aim. 168 | 169 | Your code will be reviewed and merged once it: 170 | 171 | 1. Does something useful 172 | 1. Provides a use case and example 173 | 1. Includes tests to exercise the change 174 | 1. Is up to par with surrounding code 175 | 176 | The parent project ever only contains a single branch, a branch containing the latest working version of the project. 177 | 178 | We understand and recognise that "forking" and "pull-requests" can be a daunting aspect for a beginner, so don't hesitate to ask. A pull-request should normally follow an issue where you elaborate on your desires; this is also a good place to ask about these things. 179 | 180 |
181 | 182 | ## Style 183 | 184 | Here's how we expect your code to look and feel like. 185 | 186 | ### Commits 187 | 188 | Commits should be well contained, as small as possible (but no smaller) and its messages should be in present-tense, imperative-style. 189 | 190 | E.g. 191 | 192 | ```bash 193 | # No 194 | Changed this and did that 195 | 196 | # No 197 | Changes this and does that 198 | 199 | # Yes 200 | Change this and do that 201 | ``` 202 | 203 | The reason is that, each commit is like an action. An event. And it is perfectly possible to "cherry-pick" a commit onto any given branch. In this style, it makes more sense what exactly the commit will do to your code. 204 | 205 | - Cherry pick "Add this and remove that" 206 | - Cherry pick "Remove X and replace with Y" 207 | 208 | ### Version bumping 209 | 210 | This project uses [semantic versioning](http://semver.org/) and is updated *after* a new release has been made. 211 | 212 | For example, if the project had 100 commits at the time of the latest release and has 103 commits now, then it's time to increment. If however you modify the project and it has not yet been released, then your changes are included in the overall next release. 213 | 214 | The goal is to make a new release per increment. 215 | 216 | ### Making a Release 217 | 218 | Once the project has gained features, had bugs sorted out and is in a relatively stable state, it's time to make a new release. 219 | 220 | - [https://github.com/DigitalDomain/QtPyConvert/releases](https://github.com/DigitalDomain/QtPyConvert/releases) 221 | 222 | Each release should come with: 223 | 224 | - An short summary of what has changed. 225 | - A full changelog, including links to resolved issues. 226 | 227 | 228 | 229 | 230 | 231 |
232 |
233 | Good luck and see you soon! 234 | 235 | 236 |
237 |
238 |
-------------------------------------------------------------------------------- /Dockerfile-py2.7: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | 4 | RUN apt-get update && \ 5 | apt-get install -y \ 6 | software-properties-common && \ 7 | add-apt-repository -y ppa:thopiekar/pyside-git && \ 8 | apt-get update && apt-get install -y \ 9 | python \ 10 | python-dev \ 11 | python-pip \ 12 | python-qt4 \ 13 | python-pyqt5 \ 14 | python-pyside \ 15 | python-pyside2 \ 16 | xvfb 17 | 18 | # Nose is the Python test-runner 19 | # RUN pip install -r /QtPyConvert/requirements.txt 20 | 21 | # Enable additional output from Qt.py 22 | ENV QT_VERBOSE true 23 | 24 | # Xvfb 25 | ENV DISPLAY :99 26 | ENV PYTHONPATH="${PYTHONPATH}:/workspace/QtPyConvert/src/python" 27 | 28 | WORKDIR /workspace/QtPyConvert/src/python 29 | ENTRYPOINT cp -r /QtPyConvert /workspace && \ 30 | Xvfb :99 -screen 0 1024x768x16 2>/dev/null & \ 31 | sleep 3 && \ 32 | pip install -r /workspace/QtPyConvert/requirements.txt && \ 33 | python /workspace/QtPyConvert/tests/test_core/test_binding_supported.py && \ 34 | python /workspace/QtPyConvert/tests/test_core/test_replacements.py && \ 35 | python /workspace/QtPyConvert/tests/test_psep0101/test_qsignal.py && \ 36 | python /workspace/QtPyConvert/tests/test_psep0101/test_qvariant.py && \ 37 | python /workspace/QtPyConvert/tests/test_qtcompat/test_compatibility_members.py 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. -------------------------------------------------------------------------------- /QtPyConvert_CLA_Corporate.md: -------------------------------------------------------------------------------- 1 | QtPyConvert 2 | Digial Domain 3.0 3 | 4 | Software Grant and Corporate Contributor License Agreement ("Agreement") 5 | 6 | Thank you for your interest in the QtPyConvert Project ("Project"), a Digital Domain 3.0 open source initiative. In order to clarify the intellectual property license granted with Contributions from any person or entity, Digital Domain 3.0 must have a Contributor License Agreement ("CLA" or "Agreement") on file that has been signed by each Contributor, indicating agreement to the license terms below. 7 | 8 | This CLA is modified from the Apache CLA found here: http://www.apache.org/licenses. 9 | 10 | This license is for your protection as a Contributor as well as the protection of Digital Domain 3.0 and its users; it does not change your rights to use your own Contributions for any other purpose. This version of the Agreement allows an entity (the "Corporation") to submit Contributions to Digital Domain 3.0, to authorize Contributions by its designated employees to Digital Domain 3.0, and to grant copyright and patent licenses thereto. 11 | 12 | If you have not already done so, please complete and sign, then scan and email a pdf file of this Agreement to opensource@d2.com. If necessary, send an original signed Agreement to: 13 | 14 | Digial Domain 3.0 15 | Attn: The QtPyConvert Project 16 | 12641 Beatrice Street 17 | Los Angeles, CA 90066 18 | U.S.A. 19 | 20 | Please read this document carefully before signing and keep a copy for your records. 21 | 22 | Corporation name: ________________________________________________ 23 | Corporation address: ________________________________________________ ________________________________________________ 24 | ________________________________________________ 25 | Point of Contact: ________________________________________________ 26 | E-Mail: ________________________________________________ 27 | Telephone: ________________________________________________ 28 | 29 | You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Digital Domain 3.0. Except for the license granted herein to Digital Domain 3.0 and recipients of software distributed by Digital Domain 3.0, You reserve all right, title, and interest in and to Your Contributions. 30 | 31 | 1. Definitions. 32 |
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Digital Domain 3.0. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 33 |
"Contribution" shall mean the code, documentation or other original works of authorship expressly identified in Schedule B, as well as any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Digital Domain 3.0 for inclusion in, or documentation of, any of the products owned or managed by Digital Domain 3.0 (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Digital Domain 3.0 or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Digital Domain 3.0 for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution". 34 | 35 | 2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Digital Domain 3.0 and to recipients of software distributed by Digital Domain 3.0 a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 36 | 37 | 3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Digital Domain 3.0 and to recipients of software distributed by Digital Domain 3.0 a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer Your Contribution(s) and the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 38 | 39 | 4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation designated on Schedule A below (or in a subsequent written modification to that Schedule) is authorized to submit Contributions on behalf of the Corporation. 40 | 41 | 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). 42 | 43 | 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 44 | 45 | 7. Should You wish to submit work that is not Your original creation, You may submit it to Digital Domain 3.0 separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here\]." 46 | 47 | 8. It is your responsibility to notify Digital Domain 3.0 when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with Digital Domain 3.0. 48 | 49 | 9. This Agreement DOES NOT grant permission to use the trade names, trademarks, service marks, content or product names of Digital Domain 3.0 and its affiliates, or any other contributor to the Project. 50 | 51 | Please sign: __________________________________ Date: _______________ 52 | Title: __________________________________ 53 | Corporation: __________________________________ 54 | 55 | Schedule A 56 | [Initial list of designated employees. NB: authorization is not tied to particular Contributions.\] 57 | 58 | Schedule B 59 | [Identification of optional concurrent software grant. Would be left blank or omitted if there is no concurrent software grant.\] 60 | -------------------------------------------------------------------------------- /QtPyConvert_CLA_Individual.md: -------------------------------------------------------------------------------- 1 | QtPyConvert 2 | Digial Domain 3.0 3 | 4 | Individual Contributor License Agreement ("Agreement") V2.0 5 | 6 | Thank you for your interest in the QtPyConvert Project (the "Project"), a Digial Domain 3.0 open source initiative. In order to clarify the intellectual property license granted with Contributions from any person or entity, Digial Domain 3.0 must have a Contributor License Agreement ("CLA" or "Agreement") on file that has been signed by each Contributor, indicating agreement to the license terms below. 7 | 8 | This CLA is modified from the Apache CLA found here: http://www.apache.org/licenses. 9 | 10 | This license is for your protection as a Contributor as well as the protection of Digial Domain 3.0 and its users; it does not change your rights to use your own Contributions for any other purpose. If you have not already done so, please complete and sign, then scan and email a pdf file of this Agreement to opensource@d2.com. If necessary, send an original signed Agreement to: 11 | 12 | Digial Domain 3.0 13 | Attn: The QtPyConvert Project 14 | 12641 Beatrice Street 15 | Los Angeles, CA 90066 16 | U.S.A. 17 | 18 | Please read this document carefully before signing and keep a copy for your records. 19 | 20 | Full name: _______________________________________________________ 21 | Mailing Address: _________________________________________________ 22 | _________________________________________________ 23 | Country: _________________________________________________________ 24 | Telephone: _______________________________________________________ 25 | E-Mail: __________________________________________________________ 26 | 27 | You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Digial Domain 3.0. Except for the license granted herein to Digial Domain 3.0 and recipients of software distributed by Digial Domain 3.0, You reserve all right, title, and interest in and to Your Contributions. 28 | 29 | 1. Definitions. 30 |
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Digial Domain 3.0. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 31 |
"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Digial Domain 3.0 for inclusion in, or documentation of, any of the products owned or managed by Digial Domain 3.0 (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Digial Domain 3.0 or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Digial Domain 3.0 for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 32 | 33 | 2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Digial Domain 3.0 and to recipients of software distributed by Digial Domain 3.0 a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 34 | 35 | 3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Digial Domain 3.0 and to recipients of software distributed by Digial Domain 3.0 a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer Your Contribution(s) and the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 36 | 37 | 4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Digial Domain 3.0, or that your employer has executed a separate Corporate CLA with Digial Domain 3.0. 38 | 39 | 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 40 | 41 | 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 42 | 43 | 7. Should You wish to submit work that is not Your original creation, You may submit it to Digial Domain 3.0 separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here\]". 44 | 45 | 8. You agree to notify Digial Domain 3.0 of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. 46 | 47 | 9. This Agreement DOES NOT grant permission to use the trade names, trademarks, service marks, content or product names of Digial Domain 3.0 and its affiliates, or any other contributor to the Project. 48 | 49 | 50 | Please sign: __________________________________ Date: ________________ 51 | 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # QtPyConvert
3 | 4 | * [Project Goals](#project-goals) 5 | * [Getting Started](#getting-started) 6 | * [Prerequisites](#prerequisites) 7 | * [Usage](#usage) 8 | * [Customization](#customization) 9 | * [Converting](#converting) 10 | * [Project Information](#project-information) 11 | * [Built With](#built-with) 12 | * [Contributing](#contributing) 13 | * [Versioning](#versioning) 14 | * [Authors](#authors) 15 | * [License](#license) 16 | * [Acknowledgments](#acknowledgments) 17 | 18 | 19 | 20 | 21 | An automatic Python Qt binding transpiler to the [Qt.py abstraction layer](https://github.com/mottosso/Qt.py). It aims to help in your modernization of your Python Qt code. QtPyConvert supports the following bindings out of the box: 22 | * [PyQt4](https://www.riverbankcomputing.com/software/pyqt/download) 23 | * [PySide](http://pyside.github.io/docs/pyside/) 24 | * [PyQt5](https://www.riverbankcomputing.com/software/pyqt/download5) 25 | * [PySide2](https://wiki.qt.io/PySide2) 26 | 27 | It also has experimental support for defining your own bindings. 28 | > See [customization](#customization) for more information 29 | 30 | 31 | ## Project Goals 32 | Convert any code using any of the four major Qt for Python bindings into the standardized [Qt.py abstraction layer](https://github.com/mottosso/Qt.py). 33 | 34 | Warn users about incompatibilities or unsupported code. (WIP) 35 | 36 | Standardize Qt imports to maintain sanity in code comprehension. 37 | > Removing start imports and deep class/module imports 38 | 39 | ## Getting Started 40 | 41 | When using **QtPyConvert**, developers should be aware of any [shortcomings](https://github.com/mottosso/Qt.py/blob/master/CAVEATS.md) of Qt.py or its [subset](https://github.com/mottosso/Qt.py#subset-or-common-members) of [supported features](https://github.com/mottosso/Qt.py#class-specific-compatibility-objects). 42 | 43 | Basically read the README in the Qt.py project and be aware of what it does and does not do. 44 | 45 | ### Prerequisites 46 | 47 | **QtPyConvert** reads the private values of the [Qt.py project](https://github.com/mottosso/Qt.py) to build it's internal conversion processes. To install this run 48 | ``` 49 | pip install Qt.py 50 | ``` 51 | **QtPyConvert** also uses [RedBaron](https://github.com/PyCQA/Redbaron) as an alternate abstract syntax tree. 52 | Redbaron allows us to modify the source code and write it back out again, preserving all comments and formatting. 53 | ``` 54 | pip install redbaron 55 | ``` 56 | 57 | We also provide a requirements.txt file which you could install instead by... 58 | ``` 59 | pip install -r requirements.txt 60 | ``` 61 | 62 | You should also have access to any of your source bindings so that Qy.py can import them freely. 63 | 64 | A full list of dependencies is as follows: 65 | - rebaron 66 | - baron 67 | - rply 68 | - appdirs 69 | - qt_py 70 | - argparse 71 | 72 | 73 | ### Usage 74 | ```bash 75 | $ qt_py_convert [-h] [-r] [--stdout] [--write-path WRITE_PATH] [--backup] 76 | [--show-lines] [--to-method-support] [--explicit-signals-flag] 77 | files_or_directories [files_or_directories ...] 78 | ``` 79 | 80 | | Argument | Description | 81 | | ------------------------- | ------------- | 82 | | -h,--help | Show the help message and exit. | 83 | | files_or_directories | Pass explicit files or directories to run. **NOTE:** If **"-"** is passed instead of files_or_directories, QtPyConvert will attempt to read from stdin instead. **Useful for pipelining proesses together** | 84 | | -r,--recursive | Recursively search for python files to convert. Only applicable when passing a directory. | 85 | | --stdout | Boolean flag which will write the resulting file to stdout instead of on disk. | 86 | | --write-path | If provided, QtPyConvert will treat "--write-path" as a relative root and write modified files from there. | 87 | | --backup | Create a hidden backup of the original source code beside the newly converted file. | 88 | | --show-lines | Turn on printing of line numbers while replacing statements. Ends up being much slower. | 89 | | --to-method-support | **EXPERIMENTAL**: An attempt to replace all api1.0 style "*toString*", "*toInt*", "*toBool*", "*toPyObject*", "*toAscii*" methods that are unavailable in api2.0. | 90 | | --explicit-signals-flag | **EXPERIMENTAL**: Modification on the api1.0 style signal conversion logic. It will explicitly slice into the QtCore.Signal object to find the signal with the matching signature.
This is a fairly unknown feature of PySide/PyQt and is usually worked around by the developer. However, this should be safe to turn on whichever the case. | 91 | 92 | 93 | ### Customization 94 | 95 | QtPyConvert supports some custom bindings if you are willing to do a little bit of work. 96 | 97 | This is done through environment variables: 98 | 99 | | Key | Value | Description | 100 | | ----------------------------- | ------------------------------------------------------------------------------ | ----------- | 101 | | QT_CUSTOM_BINDINGS_SUPPORT | The names of custom abstraction layers or bindings separated by **os.pathsep** | This can be used if you have code that was already doing it's own abstraction and you want to move to the Qt.py layer. | 102 | | QT_CUSTOM_MISPLACED_MEMBERS | This is a json dictionary that you have saved into your environment variables. | This json dictionary should look similar to the Qt.py _misplaced_members dictionary but instead of mapping to Qt.py it maps the source bindings to your abstraction layer. | 103 | 104 | > **Note** This feature is *experimental* and has only been used internally a few times. Support for this feature will probably be slower than support for the core functionality of QyPyConvert. 105 | 106 | ## Troubleshooting 107 | 108 | QtPyConvert is still a bit of a work in progress, there are things that it cannot yet convert with 100% certainty. 109 | The following is a guide of common problems that you might have pop up. 110 | 111 | ### During Conversion 112 | 113 | #### baron.parser.ParsingError 114 | 115 | The most common thing that you will probably see is a Baron parsing error. 116 | ```bash 117 | Traceback (most recent call last): 118 | File "/usr/bin/qt_py_convert", line 47, in 119 | main(args.file_or_directory, args.recursive, args.write) 120 | File "/usr/bin/qt_py_convert", line 40, in main 121 | process_folder(path, recursive=recursive, write=write) 122 | File "/usr/lib/python2.7/site-packages/qt_py_convert/run.py", line 310, in process_folder 123 | process_folder(os.path.join(folder, fn), recursive, write, fast_exit) 124 | File "/usr/lib/python2.7/site-packages/qt_py_convert/run.py", line 310, in process_folder 125 | process_folder(os.path.join(folder, fn), recursive, write, fast_exit) 126 | File "/usr/lib/python2.7/site-packages/qt_py_convert/run.py", line 310, in process_folder 127 | process_folder(os.path.join(folder, fn), recursive, write, fast_exit) 128 | File "/usr/lib/python2.7/site-packages/qt_py_convert/run.py", line 310, in process_folder 129 | process_folder(os.path.join(folder, fn), recursive, write, fast_exit) 130 | File "/usr/lib/python2.7/site-packages/qt_py_convert/run.py", line 302, in process_folder 131 | os.path.join(folder, fn), write=write, fast_exit=fast_exit 132 | File "/usr/lib/python2.7/site-packages/qt_py_convert/run.py", line 282, in process_file 133 | aliases, mappings, modified_code = run(source, fast_exit=fast_exit) 134 | File "/usr/lib/python2.7/site-packages/qt_py_convert/run.py", line 264, in run 135 | psep0101.process(red) 136 | File "/usr/lib/python2.7/site-packages/qt_py_convert/_modules/psep0101/process.py", line 215, in process 137 | getattr(Processes, issue)(red, psep_issues[issue]) if psep_issues[issue] else None 138 | File "/usr/lib/python2.7/site-packages/qt_py_convert/_modules/psep0101/process.py", line 34, in _process_qvariant 139 | node.parent.replace(changed) 140 | File "/usr/lib/python2.7/site-packages/redbaron/base_nodes.py", line 1016, in replace 141 | new_node = self._convert_input_to_node_object(new_node, parent=None, on_attribute=None, generic=True) 142 | File "/usr/lib/python2.7/site-packages/redbaron/base_nodes.py", line 156, in _convert_input_to_node_object 143 | return Node.from_fst(baron.parse(value)[0], parent=parent, on_attribute=on_attribute) 144 | File "/usr/lib/python2.7/site-packages/baron/baron.py", line 57, in parse 145 | to_return = _parse(tokens, print_function) 146 | File "/usr/lib/python2.7/site-packages/baron/baron.py", line 26, in _parse 147 | raise e 148 | baron.parser.ParsingError: Error, got an unexpected token RIGHT_SQUARE_BRACKET here: 149 | 1 self.user_data[index.row(]<---- here 150 | 151 | The token RIGHT_SQUARE_BRACKET should be one of those: BACKQUOTE, BINARY, BINARY_RAW_STRING, BINARY_STRING, COMMA, COMPLEX, DOUBLE_STAR, FLOAT, FLOAT_EXPONANT, FLOAT_EXPONANT_COMPLEX, HEXA, INT, LAMBDA, LEFT_BRACKET, LEFT_PARENTHESIS, LEFT_SQUARE_BRACKET, LONG, MINUS, NAME, NOT, OCTA, PLUS, RAW_STRING, RIGHT_PARENTHESIS, STAR, STRING, TILDE, UNICODE_RAW_STRING, UNICODE_STRING 152 | 153 | It is not normal that you see this error, it means that Baron has failed to parse valid Python code. It would be kind if you can extract the snippet of your code that makes Baron fail and open a bug here: https://github.com/Psycojoker/baron/issues 154 | 155 | Sorry for the inconvenience. 156 | ``` 157 | 158 | 159 | Because we are using an alternate Python AST, there are sometimes issues on edge cases of code. 160 | Unfortunately, Baron get's confused sometimes when it tries to send us a helpful traceback and the error is usually earlier than it thinks. 161 | There are two usual suspects when getting a Baron ParsingError. 162 | 163 | 164 | ##### Incorrectly indented comments 165 | 166 | This is a problem that will popup with Baron because it actually pays attention to the comments, whereas Python just throws them out. 167 | If you are getting an error similar to the above, you will want to look higher in the script for incorrectly indented comments, multiline and single line ones. 168 | 169 | ##### Bare print statements 170 | 171 | This is less common than the indented comments issue but has still shown up in a few cases for us internally. 172 | There are times where Baron cannot parse a print statement and turning it into a print function by enclosing it in parenthesis seems to fix it. 173 | 174 | 175 | #### Qt.py does not support uic.loadUiType 176 | The Qt.py module does not support uic.loadUiType. 177 | Please see [https://github.com/mottosso/Qt.py/issues/237](https://github.com/mottosso/Qt.py/issues/237) 178 | 179 | 180 | ### During Runtime 181 | 182 | #### QLayout.setMargin 183 | As of Qt 4.7 QLayout.setMargin has been set to obsolete which means that it will be removed. 184 | As it turns out, they removed it in Qt5. 185 | The obsoleted page is [here](http://doc.qt.io/archives/qt-4.8/qlayout-obsolete.html) 186 | 187 | Obsoleted code 188 | > layout = QLayout() 189 | > layout.setMargin(10) 190 | 191 | Replacement code 192 | > layout = QLayout() 193 | > layout.setContentsMargins(10, 10, 10, 10) 194 | 195 | 196 | ## Future Features 197 | 198 | There are several things that we would love to have support for in QtPyConvert but we just haven't gotten around to yet for various reasons. 199 | 200 | - Warnings about deprecated/obsoleted method calls in Qt4 code. 201 | - There were many method calls that were removed in Qt5 and most people were unaware that they should be using something else when they wrote the Qt4 code. 202 | There are lists on the Qt5 docs about what was deprecated and what the replacements are (if any) in Qt5 code. It would be nice if we could warn the user about these when we detect them and potentially automatically fix some of them too. 203 | - In a perfect world we would be able to dynamically build the list of these by scraping the Qt website too. 204 | - Better support for api-v1.0 method removal. 205 | Currently we are basially looking for the method names because they are quite unique. 206 | Ideally it'd be great if we could somehow record the type of the object at least in locals and then if that object was a QString for example, see if it is using any methods that aren't on a builtin string type. 207 | - Better support for the QtCompat..method replacements. 208 | Much of this would require the previous feature to be somewhat figured out. 209 | We would need to at least minimally track variable types. 210 | 211 | 212 | 213 | ## Project Information 214 | 215 | ### Built With 216 | 217 | * [Qt.py](https://github.com/mottosso/Qt.py) - The Qt abstraction library that we port code to. 218 | * [RedBaron](https://github.com/PyCQA/Redbaron) - The alternate Python AST which allows us to modify and preserve comments + formatting. 219 | 220 | ### Contributing 221 | 222 | Please read [CONTRIBUTING.md](https://github.com/DigitalDomain/QtPyConvert/blob/master/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 223 | 224 | ### Versioning 225 | 226 | We use [semantic versioning](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/DigitalDomain/QtPyConvert/tags). 227 | 228 | ### Authors 229 | 230 | * **Alex Hughes** - Initial work - [Digital Domain](https://digitaldomain.com) 231 | * **Rafe Sacks** - Prototyping help - [Digital Domain](https://digitaldomain.com) 232 | 233 | See also the list of [contributors](https://github.com/DigitalDomain/QtPyConvert/contributors) who participated in this project. 234 | 235 | ### License 236 | 237 | This project is licensed under a modified Apache 2.0 license - see the [LICENSE](https://github.com/DigitalDomain/QtPyConvert/blob/master/LICENSE) file for details 238 | 239 | ### Acknowledgments 240 | 241 | * [The developers of Qt.py](https://github.com/mottosso/Qt.py/contributors) 242 | * [The developers of RedBaron](https://github.com/PyCQA/redbaron/contributors) 243 | * etc 244 | -------------------------------------------------------------------------------- /_resources/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitaldomain/QtPyConvert/2a2d2b9121004c27598f9e1031cf6ebf41c0895a/_resources/logo.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Qt.py>=1.2.0.b2 2 | redbaron 3 | -------------------------------------------------------------------------------- /src/bin/qresource_convert: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2018 Digital Domain 3.0 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 5 | # with the following modification; you may not use this file except in 6 | # compliance with the Apache License and the following modification to it: 7 | # Section 6. Trademarks. is deleted and replaced with: 8 | # 9 | # 6. Trademarks. This License does not grant permission to use the trade 10 | # names, trademarks, service marks, or product names of the Licensor 11 | # and its affiliates, except as required to comply with Section 4(c) of 12 | # the License and to reproduce the content of the NOTICE file. 13 | # 14 | # You may obtain a copy of the Apache License at 15 | # 16 | # http://www.apache.org/licenses/LICENSE-2.0 17 | # 18 | # Unless required by applicable law or agreed to in writing, software 19 | # distributed under the Apache License with the above modification is 20 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 21 | # KIND, either express or implied. See the Apache License for the specific 22 | # language governing permissions and limitations under the Apache License. 23 | import argparse 24 | import contextlib 25 | from cStringIO import StringIO 26 | import os 27 | import subprocess 28 | import sys 29 | 30 | 31 | import qt_py_convert.run 32 | import qt_py_convert.general 33 | 34 | 35 | PYQT4 = "PyQt4" 36 | PYQt5 = "PyQt5" 37 | PYSIDE = "PySide" 38 | PYSIDE2 = "PySide2" 39 | PyUIC4 = "pyuic4" 40 | PyRCC4 = "pyrcc4" 41 | PyUIC5 = "pyuic5" 42 | PyRCC5 = "pyrcc5" 43 | PySideUIC = "pyside-uic" 44 | PySideRCC = "pyside-rcc" 45 | PySide2UIC = "pyside2-uic" 46 | PySide2RCC = "pyside2-rcc" 47 | 48 | QRESOURCE_ENVIRON = "QRESOURCE_CONVERTER_DEFAULT" 49 | 50 | mappings = { 51 | PYQT4: (PyUIC4, PyRCC4), 52 | PYQt5: (PyUIC5, PyRCC5), 53 | PYSIDE: (PySideUIC, PySideRCC), 54 | PYSIDE2: (PySide2UIC, PySide2RCC), 55 | } 56 | 57 | 58 | class UICConversionError(ValueError): 59 | """If we were unable to convert the uic into a python file.""" 60 | 61 | 62 | def _is_uic(path): 63 | if os.path.splitext(path)[-1] in (".ui", ".uic"): 64 | return True 65 | return False 66 | 67 | 68 | def execute_convert(application, path, output=None): 69 | if application is None: 70 | # Query Environ. 71 | application = os.environ.get(QRESOURCE_ENVIRON, PYQT4) 72 | 73 | uic, rcc = mappings[application] 74 | 75 | if _is_uic(path): 76 | binary = uic 77 | else: 78 | binary = rcc 79 | 80 | command = [binary, path] 81 | if output: 82 | command.extend(["-o", output]) 83 | proc = subprocess.Popen( 84 | command, 85 | stdout=subprocess.PIPE, 86 | stderr=subprocess.PIPE, 87 | ) 88 | out, err = proc.communicate() 89 | has_errors = False 90 | 91 | if output: 92 | message = output 93 | else: 94 | message = out 95 | 96 | if proc.returncode: 97 | has_errors = True 98 | return has_errors, err, message 99 | else: 100 | return has_errors, None, message 101 | 102 | 103 | @contextlib.contextmanager 104 | def stdout_redirector(): 105 | stdout, stderr = sys.stdout, sys.stderr 106 | out = StringIO() 107 | err = StringIO() 108 | sys.stdout = out 109 | sys.stderr = err 110 | yield 111 | sys.stdout = stdout 112 | sys.stderr = stderr 113 | 114 | 115 | def parse_args(): 116 | parser = argparse.ArgumentParser( 117 | "qresource_convert" 118 | ) 119 | 120 | parser.add_argument( 121 | "-c", "--converter", 122 | action="store", 123 | choices=tuple(mappings.keys()), 124 | default=None, 125 | dest="converter" 126 | ) 127 | parser.add_argument( 128 | "file", 129 | help="Pass explicit files or a directories to run.", 130 | ) 131 | parser.add_argument( 132 | "-o", 133 | required=False, 134 | help="Filepath to write results to." 135 | ) 136 | args = parser.parse_args() 137 | return args 138 | 139 | 140 | def main(): 141 | args = parse_args() 142 | has_errors, errors, message = execute_convert( 143 | args.converter, args.file, output=args.o 144 | ) 145 | 146 | if has_errors: 147 | raise UICConversionError(errors) 148 | 149 | if not args.o: 150 | with stdout_redirector(): 151 | # In memory 152 | _, _, output = qt_py_convert.run.run( 153 | message, skip_lineno=True, tometh_flag=True 154 | ) 155 | else: 156 | with stdout_redirector(): 157 | qt_py_convert.run.process_file( 158 | message, 159 | write_mode=qt_py_convert.general.WriteFlag.WRITE_TO_FILE, 160 | skip_lineno=True, 161 | tometh_flag=True 162 | ) 163 | output = message 164 | 165 | sys.stdout.write(output) 166 | 167 | 168 | if __name__ == "__main__": 169 | main() 170 | -------------------------------------------------------------------------------- /src/bin/qt_py_convert: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2018 Digital Domain 3.0 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 5 | # with the following modification; you may not use this file except in 6 | # compliance with the Apache License and the following modification to it: 7 | # Section 6. Trademarks. is deleted and replaced with: 8 | # 9 | # 6. Trademarks. This License does not grant permission to use the trade 10 | # names, trademarks, service marks, or product names of the Licensor 11 | # and its affiliates, except as required to comply with Section 4(c) of 12 | # the License and to reproduce the content of the NOTICE file. 13 | # 14 | # You may obtain a copy of the Apache License at 15 | # 16 | # http://www.apache.org/licenses/LICENSE-2.0 17 | # 18 | # Unless required by applicable law or agreed to in writing, software 19 | # distributed under the Apache License with the above modification is 20 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 21 | # KIND, either express or implied. See the Apache License for the specific 22 | # language governing permissions and limitations under the Apache License. 23 | import os 24 | import sys 25 | 26 | import argparse 27 | 28 | from qt_py_convert.run import process_file, process_folder 29 | from qt_py_convert.general import WriteFlag 30 | 31 | 32 | def parse(): 33 | parser = argparse.ArgumentParser( 34 | "qt_py_convert" 35 | ) 36 | 37 | parser.add_argument( 38 | "files_or_directories", 39 | nargs="+", 40 | help="Pass explicit files or a directories to run. " 41 | "NOTE: If \"-\" is passed instead of files_or_directories, " 42 | "qt_py_convert will attempt to read from stdin. " 43 | "Useful for pipelining processes" 44 | ) 45 | parser.add_argument( 46 | "-r", "--recursive", 47 | action="store_true", 48 | help="Recursively search for python files to convert. " 49 | "Only applicable when passing a directory.", 50 | ) 51 | parser.add_argument( 52 | "--stdout", 53 | action="store_true", 54 | help="Boolean flag which will write the resulting file to stdout " 55 | "instead of on disk." 56 | ) 57 | parser.add_argument( 58 | "--write-path", 59 | required=False, 60 | default=None, 61 | help="If provided, QtPyConvert will treat \"--write-path\" as a " 62 | "relative root and write modified files from there." 63 | ) 64 | parser.add_argument( 65 | "--backup", 66 | action="store_true", 67 | help="When combined with the \"--stdout\" flag it will not produce " 68 | "\"*.bak\" files for converted files.", 69 | ) 70 | parser.add_argument( 71 | "--show-lines", 72 | action="store_true", 73 | help="Turn on printing the line numbers that things are replaced at. " 74 | "Ends up being much slower to process.", 75 | ) 76 | parser.add_argument( 77 | "--to-method-support", 78 | action="store_true", 79 | help="EXPERIMENTAL: An attempt to replace all api1.0 style " 80 | "\"toString\", \"toInt\", \"toFloat\", \"toBool\", \"toPyObject\"" 81 | ", \"toAscii\" methods that are unavailable in api2.0.", 82 | ) 83 | parser.add_argument( 84 | "--explicit-signals-flag", 85 | action="store_true", 86 | help="EXPERIMENTAL: Modification on the api1.0 style signal conversion" 87 | " logic. It will explicitly slice into the QtCore.Signal object " 88 | "to find the signal with the matching signature.\n" 89 | "This is a fairly unknown feature of PySide/PyQt and is usually " 90 | "worked around by the developer. However, this should be safe to " 91 | "turn on whichever the case.", 92 | ) 93 | 94 | return parser.parse_args() 95 | 96 | 97 | def _resolve_stdin(paths): 98 | """ 99 | _resolve_stdin allows us to have "-" in our files_or_directories and have 100 | it expand to input from stdin. 101 | This allows us to pipe into qt_py_convert. 102 | 103 | :param paths: List of strings. It wll replace any strings matching "-" 104 | with sys.stdin. 105 | :type paths: list[str...] 106 | :return: List that has been modified to include stdin. 107 | :rtype: list[str...] 108 | """ 109 | _stdin = None 110 | _inserted = 0 111 | for index, path in enumerate(paths[:]): 112 | if path == "-": # Insert stdin anytime we see "-" 113 | _index = index+_inserted # Calculated index after insertions. 114 | paths.pop(_index) # Remove the "-" 115 | if _stdin is None: # If we haven't pulled from stdin yet. 116 | _stdin = [ 117 | x.strip("\n").strip("\r") # Strip \n and \r 118 | for x in list(sys.stdin) # Iterate through stdin. 119 | ] 120 | # Generate a new list. 121 | # Basically if I could do an "insert" and an "expand" at once. 122 | paths = paths[:_index] + _stdin + paths[_index:] 123 | # We have inserted x - 1 because we popped. 124 | _inserted += len(_stdin) - 1 125 | return paths 126 | 127 | 128 | def main(pathlist, recursive=True, path=None, no_write=False, backup=False, stdout=False, show_lines=True, tometh=False): 129 | # if len(pathlist) == 1: 130 | # if pathlist[0] == "-": # Support for piping on unix. 131 | # pathlist = sys.stdin 132 | 133 | pathlist = _resolve_stdin(pathlist) 134 | 135 | output = 0 136 | if stdout: 137 | output |= WriteFlag.WRITE_TO_STDOUT 138 | else: 139 | output |= WriteFlag.WRITE_TO_FILE 140 | 141 | for src_path in pathlist: 142 | # print("Processing %s" % path) 143 | abs_path = os.path.abspath(src_path) 144 | if os.path.isdir(abs_path): 145 | process_folder( 146 | src_path, 147 | recursive=recursive, 148 | write_mode=output, 149 | path=(path, abs_path), 150 | backup=backup, 151 | skip_lineno=not show_lines, 152 | tometh_flag=tometh 153 | ) 154 | else: 155 | process_file( 156 | src_path, 157 | write_mode=output, 158 | path=(path, abs_path), 159 | backup=backup, 160 | skip_lineno=not show_lines, 161 | tometh_flag=tometh 162 | ) 163 | 164 | 165 | if __name__ == "__main__": 166 | args = parse() 167 | main( 168 | pathlist=args.files_or_directories, 169 | recursive=args.recursive, 170 | path=args.write_path, 171 | backup=args.backup, 172 | stdout=args.stdout, 173 | show_lines=args.show_lines, 174 | tometh=args.to_method_support, 175 | ) 176 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | __version__ = "3.1.0" 23 | """ 24 | An automatic Python Qt binding transpiler to the Qt.py abstraction layer. 25 | It aims to help in your modernization of your Python Qt code. 26 | QtPyConvert supports the following bindings out of the box: 27 | 28 | - PyQt4 29 | - PySide 30 | - PyQt5 31 | - PySide2 32 | """ 33 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/expand_stars/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | from qt_py_convert._modules.expand_stars.process import process 23 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/expand_stars/process.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | """ 23 | The imports module is designed to fix the import statements. 24 | """ 25 | import traceback 26 | 27 | from qt_py_convert.general import ALIAS_DICT, change, supported_binding 28 | from qt_py_convert.color import color_text, ANSI 29 | from qt_py_convert.log import get_logger 30 | 31 | 32 | EXPAND_STARS_LOG = get_logger("expand_stars") 33 | 34 | 35 | class Processes(object): 36 | """Processes class for expand_stars""" 37 | @staticmethod 38 | def _get_children(binding, levels=None): 39 | """ 40 | You have done the following: 41 | >>> from . import * 42 | 43 | And I hate you a little bit. 44 | I am changing it to the following: 45 | >>> from import 46 | 47 | But I don't know what the heck you used in the * 48 | So I am just getting everything bootstrapped in. Sorry-not-sorry 49 | """ 50 | def _module_filtering(key): 51 | import __builtin__ 52 | if key.startswith("__"): 53 | return False 54 | elif key in dir(__builtin__): 55 | return False 56 | return True 57 | 58 | def _members(_mappings, _module_, module_name): 59 | members = filter(_module_filtering, dir(_module)) 60 | for member in members: 61 | mappings[member] = "{mod}.{member}".format( 62 | mod=module_name, 63 | member=member 64 | ) 65 | 66 | mappings = {} 67 | if levels is None: 68 | levels = [] 69 | try: 70 | _temp = __import__(binding, fromlist=levels) 71 | except ImportError as err: 72 | strerr = str(err).replace("No module named", "") 73 | 74 | msg = ( 75 | "Attempting to resolve a * import from the {mod} " 76 | "module failed.\n" 77 | "This is usually because the module could not be imported. " 78 | "Please check that this script can import it. The error was:\n" 79 | "{err}" 80 | ).format(mod=strerr, err=str(err)) 81 | traceback.print_exc() 82 | raise ImportError(msg) 83 | if not levels: 84 | _module = _temp 85 | _members(mappings, _module, module_name=binding) 86 | else: 87 | for level in levels: 88 | _module = getattr(_temp, level) 89 | _members(mappings, _module, module_name=level) 90 | return mappings 91 | 92 | @classmethod 93 | def _process_star(cls, red, stars, skip_lineno=False): 94 | """ 95 | _process_star is designed to replace from X import * methods. 96 | 97 | :param red: redbaron process. Unused in this method. 98 | :type red: redbardon.RedBaron 99 | :param stars: List of redbaron nodes that matched for this proc. 100 | :type stars: list 101 | """ 102 | mappings = {} 103 | for star in stars: 104 | from_import = star.parent 105 | binding = from_import.value[0] 106 | second_level_modules = None 107 | if len(star.parent.value) > 1: 108 | second_level_modules = [star.parent.value[1].dumps()] 109 | if len(star.parent.value) > 2: 110 | pass 111 | 112 | children = cls._get_children(binding.dumps(), second_level_modules) 113 | if second_level_modules is None: 114 | second_level_modules = children 115 | text = "from {binding} import {slm}".format( 116 | binding="Qt", 117 | slm=", ".join([name for name in second_level_modules]) 118 | ) 119 | 120 | change( 121 | logger=EXPAND_STARS_LOG, 122 | node=star.parent, 123 | replacement=text, 124 | skip_lineno=skip_lineno 125 | ) 126 | mappings.update(children) 127 | # star.replace( 128 | # text 129 | # ) 130 | return mappings 131 | 132 | EXPAND_STR = "EXPAND" 133 | EXPAND = _process_star 134 | 135 | 136 | def star_process(store): 137 | """ 138 | star_process is one of the more complex handlers for the _modules. 139 | 140 | :param store: Store is the issues dict defined in "process" 141 | :type store: dict 142 | :return: The filter_function callable. 143 | :rtype: callable 144 | """ 145 | def filter_function(value): 146 | for target in value.parent.targets: 147 | if target.type == "star" and supported_binding(value.dumps()): 148 | store[Processes.EXPAND_STR].add(value) 149 | return True 150 | 151 | return filter_function 152 | 153 | 154 | def process(red, skip_lineno=False, **kwargs): 155 | """ 156 | process is the main function for the import process. 157 | 158 | :param red: Redbaron ast. 159 | :type red: redbaron.redbaron 160 | :param skip_lineno: An optional performance flag. By default, when the 161 | script replaces something, it will tell you which line it is 162 | replacing on. This can be useful for tracking the places that 163 | changes occurred. When you turn this flag on however, it will not 164 | show the line numbers. This can give great performance increases 165 | because redbaron has trouble calculating the line number sometimes. 166 | :type skip_lineno: bool 167 | :param kwargs: Any other kwargs will be ignored. 168 | :type kwargs: dict 169 | """ 170 | issues = { 171 | Processes.EXPAND_STR: set(), 172 | } 173 | EXPAND_STARS_LOG.warning(color_text( 174 | text="\"import star\" used. We are bootstrapping code!", 175 | color=ANSI.colors.red, 176 | )) 177 | EXPAND_STARS_LOG.warning(color_text( 178 | text="This will be very slow. It's your own fault.", 179 | color=ANSI.colors.red, 180 | )) 181 | values = red.find_all("FromImportNode", value=star_process(issues)) 182 | 183 | mappings = getattr(Processes, Processes.EXPAND_STR)( 184 | red, issues[Processes.EXPAND_STR], skip_lineno=skip_lineno 185 | ) 186 | return ALIAS_DICT, mappings 187 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/from_imports/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | from qt_py_convert._modules.from_imports.process import process 23 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/from_imports/process.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | """ 23 | The from_imports module is designed to fix the from import statements. 24 | """ 25 | from qt_py_convert._modules.expand_stars import process as stars_process 26 | from qt_py_convert.general import __supported_bindings__, ALIAS_DICT, change, \ 27 | supported_binding 28 | from qt_py_convert.log import get_logger 29 | 30 | 31 | FROM_IMPORTS_LOG = get_logger("from_imports") 32 | IGNORED_IMPORT_TARGETS = ("right_parenthesis", "left_parenthesis") 33 | 34 | 35 | class Processes(object): 36 | """Processes class for from_imports""" 37 | @staticmethod 38 | def _get_import_parts(node, binding): 39 | return node.dumps().replace(binding, "").lstrip(".").split(".") 40 | 41 | @staticmethod 42 | def _no_second_level_module(node, _parts, skip_lineno=False): 43 | text = "from Qt import {key}".format( 44 | key=", ".join([target.value for target in node.targets]) 45 | ) 46 | 47 | change( 48 | logger=FROM_IMPORTS_LOG, 49 | node=node, 50 | replacement=text, 51 | skip_lineno=skip_lineno 52 | ) 53 | 54 | node.replace(text) 55 | 56 | @classmethod 57 | def _process_import(cls, red, objects, skip_lineno=False): 58 | """ 59 | _process_import is designed to replace from import methods. 60 | 61 | :param red: redbaron process. Unused in this method. 62 | :type red: redbardon.RedBaron 63 | :param objects: List of redbaron nodes that matched for this proc. 64 | :type objects: list 65 | :param skip_lineno: Global "skip_lineno" flag. 66 | :type skip_lineno: bool 67 | """ 68 | binding_aliases = ALIAS_DICT 69 | mappings = {} 70 | 71 | # Replace each node 72 | for node, binding in objects: 73 | from_import_parts = cls._get_import_parts(node, binding) 74 | if len(from_import_parts) and from_import_parts[0]: 75 | second_level_module = from_import_parts[0] 76 | else: 77 | cls._no_second_level_module( 78 | node.parent, 79 | from_import_parts, 80 | skip_lineno=skip_lineno 81 | ) 82 | binding_aliases["bindings"].add(binding) 83 | for target in node.parent.targets: 84 | binding_aliases["root_aliases"].add(target.value) 85 | continue 86 | 87 | for _from_as_name in node.parent.targets: 88 | if _from_as_name.type in IGNORED_IMPORT_TARGETS: 89 | continue 90 | if _from_as_name.type == "star": 91 | # TODO: Make this a flag and make use the expand module. 92 | _, star_mappings = stars_process( 93 | red 94 | ) 95 | mappings.update(star_mappings) 96 | else: 97 | key = _from_as_name.target or _from_as_name.value 98 | value = ".".join(from_import_parts)+"."+_from_as_name.value 99 | mappings[key] = value 100 | 101 | replacement = "from Qt import {key}".format( 102 | key=second_level_module 103 | ) 104 | change( 105 | logger=FROM_IMPORTS_LOG, 106 | node=node.parent, 107 | replacement=replacement, 108 | skip_lineno=skip_lineno 109 | ) 110 | node.parent.replace(replacement) 111 | binding_aliases["bindings"].add(binding) 112 | for target in node.parent.targets: 113 | binding_aliases["root_aliases"].add(target.value) 114 | if binding not in binding_aliases: 115 | binding_aliases[binding] = set() 116 | binding_aliases[binding] = binding_aliases[binding].union( 117 | set([target.value for target in node.parent.targets]) 118 | ) 119 | return binding_aliases, mappings 120 | 121 | FROM_IMPORT_STR = "FROM_IMPORT" 122 | FROM_IMPORT = _process_import 123 | 124 | 125 | def import_process(store): 126 | """ 127 | import_process is one of the more complex handlers for the _modules. 128 | 129 | :param store: Store is the issues dict defined in "process" 130 | :type store: dict 131 | :return: The filter_function callable. 132 | :rtype: callable 133 | """ 134 | def filter_function(value): 135 | """ 136 | filter_function takes an AtomTrailersNode or a DottedNameNode and will 137 | filter them out if they match something that has changed in psep0101 138 | """ 139 | _raw_module = value.dumps() 140 | # See if that import is in our __supported_bindings__ 141 | matched_binding = supported_binding(_raw_module) 142 | if matched_binding: 143 | store[Processes.FROM_IMPORT_STR].add( 144 | (value, matched_binding) 145 | ) 146 | return True 147 | 148 | return filter_function 149 | 150 | 151 | def process(red, skip_lineno=False, **kwargs): 152 | """ 153 | process is the main function for the import process. 154 | 155 | :param red: Redbaron ast. 156 | :type red: redbaron.redbaron 157 | :param skip_lineno: An optional performance flag. By default, when the 158 | script replaces something, it will tell you which line it is 159 | replacing on. This can be useful for tracking the places that 160 | changes occurred. When you turn this flag on however, it will not 161 | show the line numbers. This can give great performance increases 162 | because redbaron has trouble calculating the line number sometimes. 163 | :type skip_lineno: bool 164 | :param kwargs: Any other kwargs will be ignored. 165 | :type kwargs: dict 166 | """ 167 | issues = { 168 | Processes.FROM_IMPORT_STR: set(), 169 | } 170 | red.find_all("FromImportNode", value=import_process(issues)) 171 | 172 | key = Processes.FROM_IMPORT_STR 173 | 174 | if issues[key]: 175 | return getattr(Processes, key)(red, issues[key], skip_lineno=skip_lineno) 176 | else: 177 | return ALIAS_DICT, {} 178 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/imports/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | from qt_py_convert._modules.imports.process import process 23 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/imports/process.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | """ 23 | The imports module is designed to fix the import statements. 24 | """ 25 | from qt_py_convert.general import __supported_bindings__, ALIAS_DICT, change, \ 26 | supported_binding 27 | from qt_py_convert.log import get_logger 28 | 29 | IMPORTS_LOG = get_logger("imports") 30 | 31 | 32 | class Processes(object): 33 | """Processes class for import""" 34 | @staticmethod 35 | def _build_child_name(child): 36 | return ".".join([child_part.dumps() for child_part in child.value]) 37 | 38 | @staticmethod 39 | def _no_second_level_module(node, _child_parts, skip_lineno=False): 40 | replacement = "import Qt" 41 | change( 42 | logger=IMPORTS_LOG, 43 | node=node, 44 | replacement=replacement, 45 | skip_lineno=skip_lineno 46 | ) 47 | node.replace(replacement) 48 | 49 | @classmethod 50 | def _process_import(cls, red, objects, skip_lineno=False): 51 | """ 52 | _process_import is designed to replace import methods. 53 | 54 | :param red: redbaron process. Unused in this method. 55 | :type red: redbardon.RedBaron 56 | :param objects: List of redbaron nodes that matched for this proc. 57 | :type objects: list 58 | :param skip_lineno: Global "skip_lineno" flag. 59 | :type skip_lineno: bool 60 | """ 61 | binding_aliases = ALIAS_DICT 62 | mappings = {} 63 | 64 | # Replace each node 65 | for node, binding in objects: 66 | for child_index, child in enumerate(node): 67 | _child_name = cls._build_child_name(child) 68 | _child_as_name = child.target 69 | if _child_name.split(".")[0] not in __supported_bindings__ \ 70 | and _child_as_name not in __supported_bindings__: 71 | # Only one of our multi import node's children is relevant. 72 | continue 73 | 74 | _child_parts = _child_name.replace(binding, "") 75 | _child_parts = _child_parts.lstrip(".").split(".") 76 | 77 | # Check to see if there is a second level module 78 | if len(_child_parts) and _child_parts[0]: 79 | second_level_module = _child_parts[0] 80 | else: 81 | if len(node) == 1: 82 | # Only one in the import: "import PySide" 83 | cls._no_second_level_module(node.parent, _child_parts) 84 | else: 85 | # Multiple in the import: "import PySide, os" 86 | node_parent = node.parent 87 | node.pop(child_index) 88 | repl = node.parent.dumps() + "\nimport Qt" 89 | 90 | change( 91 | logger=IMPORTS_LOG, 92 | node=node_parent, 93 | replacement=repl, 94 | skip_lineno=skip_lineno 95 | ) 96 | 97 | node.parent.replace(repl) 98 | if _child_as_name: 99 | mappings[_child_as_name] = "Qt" 100 | else: 101 | mappings[_child_name] = "Qt" 102 | binding_aliases["bindings"].add(binding) 103 | continue 104 | 105 | mappings[_child_as_name or _child_name] = ".".join( 106 | _child_parts 107 | ) 108 | 109 | change( 110 | logger=IMPORTS_LOG, 111 | node=node.parent, 112 | replacement="from Qt import {key}".format( 113 | key=second_level_module 114 | ), 115 | skip_lineno=skip_lineno 116 | ) 117 | 118 | node.parent.replace( 119 | "from Qt import {key}".format(key=second_level_module) 120 | ) 121 | binding_aliases["bindings"].add(binding) 122 | binding_aliases["root_aliases"].add(second_level_module) 123 | if binding not in binding_aliases: 124 | binding_aliases[binding] = set() 125 | binding_aliases[binding].add(second_level_module) 126 | return binding_aliases, mappings 127 | 128 | IMPORT_STR = "IMPORT" 129 | IMPORT = _process_import 130 | 131 | 132 | def import_process(store): 133 | """ 134 | import_process is one of the more complex handlers for the _modules. 135 | 136 | :param store: Store is the issues dict defined in "process" 137 | :type store: dict 138 | :return: The filter_function callable. 139 | :rtype: callable 140 | """ 141 | def filter_function(value): 142 | """ 143 | filter_function takes an AtomTrailersNode or a DottedNameNode and will filter them out if they match something that 144 | has changed in psep0101 145 | """ 146 | _raw_module = value.dumps().split(".")[0] 147 | # See if that import is in our __supported_bindings__ 148 | matched_binding = supported_binding(_raw_module) 149 | if matched_binding: 150 | store[Processes.IMPORT_STR].add( 151 | (value, matched_binding) 152 | ) 153 | return True 154 | return filter_function 155 | 156 | 157 | def process(red, skip_lineno=False, **kwargs): 158 | """ 159 | process is the main function for the import process. 160 | 161 | :param red: Redbaron ast. 162 | :type red: redbaron.redbaron 163 | :param skip_lineno: An optional performance flag. By default, when the 164 | script replaces something, it will tell you which line it is 165 | replacing on. This can be useful for tracking the places that 166 | changes occurred. When you turn this flag on however, it will not 167 | show the line numbers. This can give great performance increases 168 | because redbaron has trouble calculating the line number sometimes. 169 | :type skip_lineno: bool 170 | :param kwargs: Any other kwargs will be ignored. 171 | :type kwargs: dict 172 | """ 173 | issues = { 174 | Processes.IMPORT_STR: set(), 175 | } 176 | red.find_all("ImportNode", value=import_process(issues)) 177 | key = Processes.IMPORT_STR 178 | 179 | if issues[key]: 180 | return getattr(Processes, key)(red, issues[key], skip_lineno=skip_lineno) 181 | else: 182 | return ALIAS_DICT, {} 183 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/psep0101/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | from qt_py_convert._modules.psep0101.process import process 23 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/psep0101/_c_args.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | try: 23 | import __builtin__ as builtins 24 | except ImportError: 25 | import builtins 26 | 27 | def pythonize_arg(arg): 28 | if arg in dir(builtins): 29 | return arg 30 | elif "[" in arg or "list" in arg.lower(): 31 | return "list" 32 | elif arg == "QString": 33 | return "str" 34 | elif arg == "QVariant": 35 | return "object" 36 | elif arg.startswith("Q"): 37 | return arg 38 | else: 39 | return "object" 40 | 41 | 42 | def parse_args(arg_str): 43 | args = arg_str.split(",") 44 | _final = [] 45 | for arg in args: 46 | if not arg: 47 | continue 48 | arg_c = arg.strip("&").strip().split(" ")[-1] 49 | 50 | arg_c = pythonize_arg(arg_c) 51 | 52 | _final.append(arg_c) 53 | final_args = ", ".join(_final) 54 | return final_args 55 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/psep0101/_conversion_methods.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | import re 23 | 24 | 25 | def to_methods(function_str): 26 | """ 27 | to_methods is a helper function that aims to replace all the "toX" methods 28 | from PyQt4-apiV1.0. 29 | 30 | :param function_str: String that represents something that may have the 31 | toX methods in it. 32 | :type function_str: str 33 | :return: A string that, if a method was found, has been cleaned. 34 | :rtype: str 35 | """ 36 | match = re.match( 37 | r""" 38 | (?P.*?) # Whatever was before it. 39 | \.to(?: # Get all the options of. 40 | String| # toString 41 | Int| # toInt 42 | Float| # toFloat 43 | Bool| # toBool 44 | PyObject| # toPyObject 45 | Ascii # toAscii 46 | )\(.*?\)(?P.*)""", 47 | function_str, 48 | re.VERBOSE | re.MULTILINE 49 | ) 50 | if match: 51 | replacement = match.groupdict()["object"] 52 | replacement += match.groupdict()["end"] 53 | return replacement 54 | return function_str 55 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/psep0101/_qsignal.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | """ 23 | _qsignal module has helper replacement methods for the psep0101 qsignal 24 | replacement methods. 25 | 26 | It uses _c_args to attempt o parse C style args from api v1.0 27 | """ 28 | import re 29 | 30 | from qt_py_convert._modules.psep0101._c_args import parse_args 31 | 32 | 33 | def _connect_repl(match_obj, explicit=False): 34 | template = r"{owner}.{signal}.connect({slot})" 35 | groups = match_obj.groupdict() 36 | if "strslot" in groups and groups["strslot"]: 37 | template = template.replace("{slot}", "{slot_owner}.{strslot}") 38 | 39 | if "slot_owner" not in groups or not groups["slot_owner"]: 40 | template = template.replace("{slot_owner}", "{root}") 41 | if "owner" not in groups or not groups["owner"]: 42 | template = template.replace("{owner}", "{root}") 43 | 44 | if "signal_args" in groups and groups["signal_args"]: 45 | groups["signal_args"] = parse_args(groups["signal_args"] or "") 46 | if explicit: 47 | template = template.replace("{signal}", "{signal}[{signal_args}]") 48 | return template.format(**groups) 49 | 50 | 51 | def _disconnect_repl(match_obj, explicit=False): 52 | template = r"{owner}.{signal}.disconnect({slot})" 53 | groups = match_obj.groupdict() 54 | if "strslot" in groups and groups["strslot"]: 55 | template = template.replace("{slot}", "{root}.{strslot}") 56 | 57 | if "signal_args" in groups and groups["signal_args"]: 58 | groups["signal_args"] = parse_args(groups["signal_args"] or "") 59 | if explicit: 60 | template = template.replace("{signal}", "{signal}[{signal_args}]") 61 | return template.format(**groups) 62 | 63 | 64 | def _emit_repl(match_obj, explicit=False): 65 | template = r"{owner}.{signal}.emit({args})" 66 | groups = match_obj.groupdict() 67 | 68 | if "owner" not in groups or not groups["owner"]: 69 | template = template.replace("{owner}", "{root}") 70 | 71 | if groups["signal_args"] and explicit: 72 | groups["signal_args"] = parse_args(groups["signal_args"]) 73 | template = template.replace("{signal}", "{signal}[{signal_args}]") 74 | 75 | groups["args"] = groups["args"] or "" 76 | return template.format(**groups) 77 | 78 | 79 | def process_connect(function_str, explicit=False): 80 | SIGNAL_RE = re.compile( 81 | r""" 82 | (?P[\w\.]+)?\.connect(?:\s+)?\((?:[\s\n]+)? 83 | 84 | # Making the owner optional. 85 | # _connect_repl has been updated to use root if owner is missing. 86 | (?:(?P.*?),(?:[\s\n]+)?)? 87 | 88 | (?:QtCore\.)?SIGNAL(?:\s+)?(?:\s+)?\((?:[\s\n]+)?(?:_fromUtf8(?:\s+)?\()?(?:[\s\n]+)?[\'\"](?P\w+)(?:(?:\s+)?\((?P.*?)\))?[\'\"](?:[\s\n]+)?\)?(?:[\s\n]+)?\),(?:[\s\n]+)? 89 | 90 | # Either QtCore.SLOT("thing()") or an actual callable in scope. 91 | # If it is the former, we are assuming that the str name is owned by root. 92 | (?:(?:(?P.*>?),(?:[\s\n]+)?)?(?:(?:QtCore\.)?SLOT(?:\s+)?\((?:[\s\n]+)?(?:_fromUtf8(?:\s+)?\()?(?:[\s\n]+)?[\'\"](?P.*?)(?:\s+)?\((?P.*?)\)[\'\"](?:[\s\n]+)?\)?(?:[\s\n]+)?\)) 93 | | 94 | (?:(?:[\s\n]+)?(?P.*?)(?:,)?(?:[\s\n]+)?)) 95 | \)""", 96 | re.VERBOSE | re.MULTILINE 97 | ) 98 | # match = SIGNAL_RE.search(function_str) 99 | replacement_str = SIGNAL_RE.sub( 100 | lambda match: _connect_repl(match, explicit=explicit), 101 | function_str 102 | ) 103 | if replacement_str != function_str: 104 | return replacement_str 105 | return function_str 106 | 107 | 108 | def process_disconnect(function_str, explicit=False): 109 | """ 110 | 'self.disconnect(self, QtCore.SIGNAL("textChanged()"), self.slot_textChanged)', 111 | "self.textChanged.disconnect(self.slot_textChanged)" 112 | """ 113 | SIGNAL_RE = re.compile( 114 | r""" 115 | (?P[\w\.]+)?\.disconnect(?:\s+)?\((?:[\s\n]+)? 116 | (?P.*?),(?:[\s\n]+)? 117 | (?:QtCore\.)?SIGNAL(?:\s+)?\((?:[\s\n]+)?(?:_fromUtf8(?:\s+)?(?:\s+)?\()?(?:[\s\n]+)?[\'\"](?P\w+)(?:\s+)?\((?P.*?)(?:\s+)?\)[\'\"](?:[\s\n]+)?\)?(?:[\s\n]+)?\),(?:[\s\n]+)? 118 | 119 | # Either QtCore.SLOT("thing()") or an actual callable in scope. 120 | # If it is the former, we are assuming that the str name is owned by root. 121 | (?:(?:(?P.*>?),(?:[\s\n]+)?)?(?:(?:QtCore\.)?SLOT(?:\s+)?\((?:[\s\n]+)?(?:_fromUtf8(?:\s+)?\()?(?:[\s\n]+)?[\'\"](?P.*?)(?:\s+)?\((?P.*?)(?:\s+)?\)[\'\"](?:[\s\n]+)?\)?(?:[\s\n]+)?\)) 122 | | 123 | (?:(?:[\s\n]+)?(?P.*?)(?:,)?(?:[\s\n]+)?)) 124 | (?:\s+)?\)""", 125 | re.VERBOSE 126 | ) 127 | replacement_str = SIGNAL_RE.sub( 128 | lambda match: _disconnect_repl(match, explicit=explicit), 129 | function_str 130 | ) 131 | if replacement_str != function_str: 132 | return replacement_str 133 | return function_str 134 | 135 | 136 | def process_emit(function_str, explicit=False): 137 | SIGNAL_RE = re.compile( 138 | r""" 139 | (?P[\w\.]+)?\.emit(?:\s+)?\((?:[\s\n]+)? 140 | (?:(?P.*?),(?:[\s\n]+)?)? 141 | (?:QtCore\.)?SIGNAL(?:\s+)?(?:\s+)?\((?:[\s\n]+)?(?:_fromUtf8(?:\s+)?\()?(?:[\s\n]+)?[\'\"](?P\w+)(?:(?:\s+)?\((?P.*?)\))?[\'\"](?:[\s\n]+)?\)?(?:[\s\n]+)?\) 142 | 143 | # Getting the args. 144 | (?:,(?:[\s\n]+)?(?:[\s\n]+)?(?P.*) 145 | (?:\s+)?)?(?:[\s\n]+)?(?:\s+)?\)""", 146 | re.VERBOSE 147 | ) 148 | replacement_str = SIGNAL_RE.sub( 149 | lambda match: _emit_repl(match, explicit=explicit), 150 | function_str 151 | ) 152 | if replacement_str != function_str: 153 | return replacement_str 154 | return function_str 155 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/psep0101/process.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | """ 23 | The psep0101 module is designed to make the changes set out in psep0101. 24 | 25 | These are the changes that created "api v2.0", which is what PySide uses and 26 | PyQt4 has the option of. 27 | """ 28 | __author__ = 'ahughes' 29 | # https://github.com/techtonik/pseps 30 | 31 | import re 32 | import sys 33 | 34 | from qt_py_convert.general import change, ErrorClass 35 | from qt_py_convert.log import get_logger 36 | from qt_py_convert._modules.psep0101 import _qsignal 37 | from qt_py_convert._modules.psep0101 import _conversion_methods 38 | 39 | 40 | PSEP_LOG = get_logger("psep0101") 41 | 42 | # Pulled out of six because I don't want to have to bind this package to DD 43 | # code to load six. 44 | # That seems a little insane to me...So because I am only using six.text_type, 45 | # I am removing the six import and inlining the code. 46 | # TODO: rely on six when this is OSS 47 | text_type = str if sys.version_info[0] == 3 else unicode 48 | 49 | 50 | class Processes(object): 51 | """Processes class for psesp0101""" 52 | @staticmethod 53 | def _process_qvariant(red, objects, skip_lineno=False, **kwargs): 54 | """ 55 | _process_qvariant is designed to replace QVariant code. 56 | 57 | :param red: redbaron process. Unused in this method. 58 | :type red: redbardon.RedBaron 59 | :param objects: List of redbaron nodes that matched for this proc. 60 | :type objects: list 61 | :param skip_lineno: Global "skip_lineno" flag. 62 | :type skip_lineno: bool 63 | """ 64 | qvariant_expr = re.compile( 65 | r"(?:QtCore\.)?QVariant(?P\((?P.*)\))?" 66 | ) 67 | 68 | # Replace each node 69 | for node in objects: 70 | raw = node.parent.dumps() 71 | matched = qvariant_expr.search(raw) 72 | if matched: 73 | if not matched.groupdict()["is_instance"]: 74 | # We have the usage of a QVariant Class object. 75 | # This leads to an invalid statement and cannot be 76 | # resolved in api 2.0. 77 | # We are adding it to warnings and continuing on. 78 | ErrorClass.from_node( 79 | node=node, 80 | reason=""" 81 | As of api v2.0, there is no concept of a "QVariant" object. 82 | Usage of the class object directly cannot be translated into something that \ 83 | can consistantly be relied on. 84 | 85 | You will probably want to remove the usage of this entirely.""" 86 | ) 87 | continue 88 | else: # If it was used as an instance (most cases). 89 | def replacement(match): 90 | """regex sub function""" 91 | # Some edge case logic here. 92 | # Was having issues replacing the following code: 93 | # return QtCore.QVariant() 94 | # There was no parameter...So now that becomes: 95 | # return None 96 | if not match.groupdict()["value"]: 97 | return "None" 98 | return match.groupdict()["value"] 99 | 100 | # We have an instance of a QVariant used. 101 | changed = qvariant_expr.sub( 102 | replacement, raw 103 | ) 104 | 105 | if changed != raw: 106 | change( 107 | logger=PSEP_LOG, 108 | node=node.parent, 109 | replacement=changed.strip(" "), 110 | skip_lineno=skip_lineno, 111 | ) 112 | node.parent.replace(changed.strip(" ")) 113 | 114 | @staticmethod 115 | def _process_qstring(red, objects, skip_lineno=False, **kwargs): 116 | """ 117 | _process_qstring is designed to replace QString code. 118 | 119 | :param red: redbaron process. Unused in this method. 120 | :type red: redbardon.RedBaron 121 | :param objects: List of redbaron nodes that matched for this proc. 122 | :type objects: list 123 | :param skip_lineno: Global "skip_lineno" flag. 124 | :type skip_lineno: bool 125 | """ 126 | # Replace each node 127 | for node in objects: 128 | raw = node.parent.dumps() 129 | changed = re.sub( 130 | r"((?:QtCore\.)?QString(?:\.fromUtf8)?)", 131 | text_type.__name__, 132 | raw 133 | ) 134 | if changed != raw: 135 | change( 136 | logger=PSEP_LOG, 137 | node=node.parent, 138 | replacement=changed, 139 | skip_lineno=skip_lineno, 140 | ) 141 | 142 | node.parent.replace(changed) 143 | 144 | @staticmethod 145 | def _process_qstringlist(red, objects, skip_lineno=False, **kwargs): 146 | """ 147 | _process_qstringlist is designed to replace QStringList code. 148 | 149 | :param red: redbaron process. Unused in this method. 150 | :type red: redbardon.RedBaron 151 | :param objects: List of redbaron nodes that matched for this proc. 152 | :type objects: list 153 | :param skip_lineno: Global "skip_lineno" flag. 154 | :type skip_lineno: bool 155 | """ 156 | # TODO: Find different usage cases of QStringList. 157 | # Probably just need support for construction and isinstance. 158 | # Replace each node 159 | for node in objects: 160 | raw = node.parent.dumps() 161 | changed = re.sub( 162 | r"((?:QtCore\.)?QStringList)", 163 | "list", 164 | raw 165 | ) 166 | if changed != raw: 167 | change( 168 | logger=PSEP_LOG, 169 | node=node.parent, 170 | replacement=changed, 171 | skip_lineno=skip_lineno, 172 | ) 173 | 174 | node.parent.replace(changed) 175 | 176 | @staticmethod 177 | def _process_qchar(red, objects, skip_lineno=False, **kwargs): 178 | """ 179 | _process_qchar is designed to replace QChar code. 180 | 181 | :param red: redbaron process. Unused in this method. 182 | :type red: redbardon.RedBaron 183 | :param objects: List of redbaron nodes that matched for this proc. 184 | :type objects: list 185 | :param skip_lineno: Global "skip_lineno" flag. 186 | :type skip_lineno: bool 187 | """ 188 | # Replace each node 189 | for node in objects: 190 | raw = node.parent.dumps() 191 | changed = re.sub( 192 | r"((?:QtCore\.)?QChar)", 193 | text_type.__name__, 194 | raw 195 | ) 196 | if changed != raw: 197 | change( 198 | logger=PSEP_LOG, 199 | node=node.parent, 200 | replacement=changed, 201 | skip_lineno=skip_lineno, 202 | ) 203 | 204 | node.parent.replace(changed) 205 | 206 | @staticmethod 207 | def _process_to_methods(red, objects, skip_lineno=False, **kwargs): 208 | """ 209 | Attempts at fixing the "toString" "toBool" "toPyObject" etc 210 | PyQt4-apiv1.0 helper methods. 211 | 212 | :param red: redbaron process. Unused in this method. 213 | :type red: redbardon.RedBaron 214 | :param objects: List of redbaron nodes that matched for this proc. 215 | :type objects: list 216 | :param skip_lineno: Global "skip_lineno" flag. 217 | :type skip_lineno: bool 218 | """ 219 | for node in objects: 220 | raw = node.parent.dumps() 221 | changed = _conversion_methods.to_methods(raw) 222 | if changed != raw: 223 | change( 224 | logger=PSEP_LOG, 225 | node=node.parent, 226 | replacement=changed, 227 | skip_lineno=skip_lineno, 228 | ) 229 | 230 | node.parent.replace(changed) 231 | continue 232 | 233 | @staticmethod 234 | def _process_qsignal(red, objects, skip_lineno=False, explicit_signals_flag=False): 235 | """ 236 | _process_qsignal is designed to replace QSignal code. 237 | It calls out to the _qsignal module and can fix disconnects, connects, 238 | and emits. 239 | 240 | :param red: redbaron process. Unused in this method. 241 | :type red: redbardon.RedBaron 242 | :param objects: List of redbaron nodes that matched for this proc. 243 | :type objects: list 244 | :param skip_lineno: Global "skip_lineno" flag. 245 | :type skip_lineno: bool 246 | """ 247 | for node in objects: 248 | raw = node.parent.dumps() 249 | 250 | if "disconnect" in raw: 251 | changed = _qsignal.process_disconnect(raw, explicit=explicit_signals_flag) 252 | if changed != raw: 253 | change( 254 | logger=PSEP_LOG, 255 | node=node.parent, 256 | replacement=changed, 257 | skip_lineno=skip_lineno, 258 | ) 259 | 260 | node.parent.replace(changed) 261 | continue 262 | if "connect" in raw: 263 | changed = _qsignal.process_connect(raw, explicit=explicit_signals_flag) 264 | if changed != raw: 265 | change( 266 | logger=PSEP_LOG, 267 | node=node.parent, 268 | replacement=changed, 269 | skip_lineno=skip_lineno, 270 | ) 271 | node.parent.replace(changed) 272 | continue 273 | if "emit" in raw: 274 | changed = _qsignal.process_emit(raw, explicit=explicit_signals_flag) 275 | if changed != raw: 276 | 277 | change( 278 | logger=PSEP_LOG, 279 | node=node.parent, 280 | replacement=changed, 281 | skip_lineno=skip_lineno, 282 | ) 283 | node.parent.replace(changed) 284 | continue 285 | 286 | @staticmethod 287 | def _process_qstringref(red, objects, skip_lineno=False, **kwargs): 288 | """ 289 | _process_qstringref is designed to replace QStringRefs 290 | 291 | :param red: redbaron process. Unused in this method. 292 | :type red: redbardon.RedBaron 293 | :param objects: List of redbaron nodes that matched for this proc. 294 | :type objects: list 295 | :param skip_lineno: Global "skip_lineno" flag. 296 | :type skip_lineno: bool 297 | """ 298 | # Replace each node 299 | for node in objects: 300 | raw = node.parent.dumps() 301 | changed = re.sub( 302 | r"((?:QtCore\.)?QStringRef)", 303 | text_type.__name__, 304 | raw 305 | ) 306 | if changed != raw: 307 | change( 308 | logger=PSEP_LOG, 309 | node=node.parent, 310 | replacement=changed, 311 | skip_lineno=skip_lineno, 312 | ) 313 | node.parent.replace(changed) 314 | 315 | QSTRING_PROCESS_STR = "QSTRING_PROCESS" 316 | QSTRINGLIST_PROCESS_STR = "QSTRINGLIST_PROCESS" 317 | QCHAR_PROCESS_STR = "QCHAR_PROCESS" 318 | QSTRINGREF_PROCESS_STR = "QSTRINGREF_PROCESS" 319 | QSTRING_PROCESS = _process_qstring 320 | QSTRINGLIST_PROCESS = _process_qstringlist 321 | QCHAR_PROCESS = _process_qchar 322 | QSTRINGREF_PROCESS = _process_qstringref 323 | QSIGNAL_PROCESS_STR = "QSIGNAL_PROCESS" 324 | QSIGNAL_PROCESS = _process_qsignal 325 | QVARIANT_PROCESS_STR = "QVARIANT_PROCESS" 326 | QVARIANT_PROCESS = _process_qvariant 327 | TOMETHOD_PROCESS_STR = "TOMETHOD_PROCESS" 328 | TOMETHOD_PROCESS = _process_to_methods 329 | 330 | 331 | def psep_process(store): 332 | """ 333 | psep_process is one of the more complex handlers for the _modules. 334 | 335 | :param store: Store is the psep_issues dict defined in "process" 336 | :type store: dict 337 | :return: The filter_function callable. 338 | :rtype: callable 339 | """ 340 | _qstring_expression = re.compile( 341 | r"QString(?:[^\w]+(?:.*?))+?$" 342 | ) 343 | _qstringlist_expression = re.compile( 344 | r"QStringList(?:[^\w]+(?:.*?))+?$" 345 | ) 346 | _qchar_expression = re.compile( 347 | r"QChar(?:[^\w]+(?:.*?))+?$" 348 | ) 349 | _qstringref_expression = re.compile( 350 | r"QStringRef(?:[^\w]+(?:.*?))+?$" 351 | ) 352 | _qsignal_expression = re.compile( 353 | r"(?:connect|disconnect|emit).*(QtCore\.)?SIGNAL", re.DOTALL 354 | ) 355 | _qvariant_expression = re.compile( 356 | r"^QVariant(?:[^\w]+(?:.*?))?$" 357 | ) 358 | _to_method_expression = re.compile( 359 | r"to[A-Z][A-Za-z]+\(\)" 360 | ) 361 | 362 | def filter_function(value): 363 | """ 364 | filter_function takes an AtomTrailersNode or a DottedNameNode and will 365 | filter them out if they match something that has changed in psep0101. 366 | """ 367 | found = False 368 | if _qstring_expression.search(value.dumps()): 369 | store[Processes.QSTRING_PROCESS_STR].add(value) 370 | found = True 371 | if _qstringlist_expression.search(value.dumps()): 372 | store[Processes.QSTRINGLIST_PROCESS_STR].add(value) 373 | found = True 374 | if _qchar_expression.search(value.dumps()): 375 | store[Processes.QCHAR_PROCESS_STR].add(value) 376 | found = True 377 | if _qstringref_expression.search(value.dumps()): 378 | store[Processes.QSTRINGREF_PROCESS_STR].add(value) 379 | found = True 380 | if _qsignal_expression.search(value.dumps()): 381 | store[Processes.QSIGNAL_PROCESS_STR].add(value) 382 | found = True 383 | if _qvariant_expression.search(value.dumps()): 384 | store[Processes.QVARIANT_PROCESS_STR].add(value) 385 | found = True 386 | if Processes.TOMETHOD_PROCESS_STR in store: 387 | if _to_method_expression.search(value.dumps()): 388 | store[Processes.TOMETHOD_PROCESS_STR].add(value) 389 | found = True 390 | if found: 391 | return True 392 | return filter_function 393 | 394 | 395 | def process(red, skip_lineno=False, tometh_flag=False, explicit_signals_flag=False, **kwargs): 396 | """ 397 | process is the main function for the psep0101 process. 398 | 399 | :param red: Redbaron ast. 400 | :type red: redbaron.redbaron 401 | :param skip_lineno: An optional performance flag. By default, when the 402 | script replaces something, it will tell you which line it is 403 | replacing on. This can be useful for tracking the places that 404 | changes occurred. When you turn this flag on however, it will not 405 | show the line numbers. This can give great performance increases 406 | because redbaron has trouble calculating the line number sometimes. 407 | :type skip_lineno: bool 408 | :param tometh_flag: tometh_flag is an optional feature flag. Once turned 409 | on, it will attempt to replace any QString/QVariant/etc apiv1.0 methods 410 | that are being used in your script. It is currently not smart enough to 411 | confirm that you don't have any custom objects with the same method 412 | signature to PyQt4's apiv1.0 ones. 413 | :type tometh_flag: bool 414 | :param kwargs: Any other kwargs will be ignored. 415 | :type kwargs: dict 416 | """ 417 | psep_issues = { 418 | Processes.QSTRING_PROCESS_STR: set(), 419 | Processes.QSTRINGLIST_PROCESS_STR: set(), 420 | Processes.QCHAR_PROCESS_STR: set(), 421 | Processes.QSTRINGREF_PROCESS_STR: set(), 422 | Processes.QSIGNAL_PROCESS_STR: set(), 423 | Processes.QVARIANT_PROCESS_STR: set(), 424 | } 425 | 426 | # Start running the to_method_process if we turn on the flag. 427 | if tometh_flag: 428 | psep_issues[Processes.TOMETHOD_PROCESS_STR] = set() 429 | 430 | red.find_all("AtomTrailersNode", value=psep_process(psep_issues)) 431 | red.find_all("DottedNameNode", value=psep_process(psep_issues)) 432 | 433 | name_nodes = red.find_all("NameNode") 434 | filter_function = psep_process(psep_issues) 435 | for name in name_nodes: 436 | filter_function(name) 437 | 438 | for issue in psep_issues: 439 | if psep_issues[issue]: 440 | getattr(Processes, issue)( 441 | red, 442 | psep_issues[issue], 443 | skip_lineno=skip_lineno, 444 | explicit_signals_flag=explicit_signals_flag 445 | ) 446 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/unsupported/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | from qt_py_convert._modules.unsupported.process import process 23 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/_modules/unsupported/process.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | import re 23 | 24 | from qt_py_convert.general import ALIAS_DICT, ErrorClass 25 | 26 | 27 | class Processes(object): 28 | 29 | @staticmethod 30 | def _process_load_ui_type(red, objects, skip_lineno=False): 31 | for node in objects: 32 | ErrorClass.from_node( 33 | node=node, 34 | reason=""" 35 | The Qt.py module does not support uic.loadUiType as it is not a method in PySide. 36 | Please see: https://github.com/mottosso/Qt.py/issues/237 37 | For more information. 38 | 39 | This will break and you will have to update this or refactor it out.""" 40 | ) 41 | 42 | LOADUITYPE_STR = "LOADUITYPE" 43 | LOADUITYPE = _process_load_ui_type 44 | 45 | 46 | def unsupported_process(store): 47 | """ 48 | unsupported_process is one of the more complex handlers for the _modules. 49 | 50 | :param store: Store is the issues dict defined in "process" 51 | :type store: dict 52 | :return: The filter_function callable. 53 | :rtype: callable 54 | """ 55 | _loaduitype_expression = re.compile( 56 | r"(?:uic\.)?loadUiType", re.DOTALL 57 | ) 58 | 59 | def filter_function(value): 60 | """ 61 | filter_function takes an AtomTrailersNode or a DottedNameNode and will 62 | filter them out if they match something that is unsupported in Qt.py 63 | """ 64 | found = False 65 | if _loaduitype_expression.search(value.dumps()): 66 | store[Processes.LOADUITYPE_STR].add(value) 67 | found = True 68 | if found: 69 | return True 70 | return filter_function 71 | 72 | 73 | def process(red, skip_lineno=False, **kwargs): 74 | """ 75 | process is the main function for the import process. 76 | 77 | :param red: Redbaron ast. 78 | :type red: redbaron.redbaron 79 | :param skip_lineno: An optional performance flag. By default, when the 80 | script replaces something, it will tell you which line it is 81 | replacing on. This can be useful for tracking the places that 82 | changes occurred. When you turn this flag on however, it will not 83 | show the line numbers. This can give great performance increases 84 | because redbaron has trouble calculating the line number sometimes. 85 | :type skip_lineno: bool 86 | :param kwargs: Any other kwargs will be ignored. 87 | :type kwargs: dict 88 | """ 89 | issues = { 90 | Processes.LOADUITYPE_STR: set(), 91 | } 92 | 93 | red.find_all("AtomTrailersNode", value=unsupported_process(issues)) 94 | red.find_all("DottedNameNode", value=unsupported_process(issues)) 95 | key = Processes.LOADUITYPE_STR 96 | 97 | if issues[key]: 98 | return getattr(Processes, key)(red, issues[key], 99 | skip_lineno=skip_lineno) 100 | else: 101 | return ALIAS_DICT, {} 102 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/color.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | import os 23 | import subprocess 24 | import sys 25 | 26 | 27 | class ANSI(object): 28 | """ANSI is a namespace object. Useful for passing values into "_color" """ 29 | class colors(object): 30 | white = 29 31 | black = 30 32 | red = 31 33 | green = 32 34 | orange = 33 35 | blue = 34 36 | purple = 35 37 | teal = 36 38 | gray = 37 39 | 40 | class styles(object): 41 | plain = 0 42 | strong = 1 43 | underline = 4 44 | reversed = 7 45 | strike = 9 46 | 47 | 48 | def supports_color(): 49 | """ 50 | Returns True if the running system's terminal supports color, and False 51 | otherwise. 52 | """ 53 | plat = sys.platform 54 | supported_platform = plat != 'Pocket PC' and (plat != 'win32' or 55 | 'ANSICON' in os.environ) 56 | try: 57 | p = subprocess.Popen( 58 | ["tput", "colors"], stdout=subprocess.PIPE, stderr=subprocess.PIPE 59 | ) 60 | has_colors = p.communicate()[0].strip(b"\n") 61 | try: 62 | has_colors = int(has_colors) 63 | except: 64 | has_colors = False 65 | except OSError: # Cannot find tput on windows 66 | return False 67 | 68 | # isatty is not always implemented, #6223. 69 | is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() 70 | if (not supported_platform or not is_a_tty) and not has_colors: 71 | return False 72 | return True 73 | 74 | 75 | SUPPORTS_COLOR = supports_color() 76 | 77 | 78 | def color_text(color=ANSI.colors.white, text="", style=ANSI.styles.plain): 79 | """ 80 | _color will print the ansi text coloring code for the text. 81 | 82 | :param color: Ansi color code number. 83 | :type color: int 84 | :param text: Text that you want colored. 85 | :type text: str 86 | :return: The colored version of the text 87 | :rtype: str 88 | """ 89 | if not SUPPORTS_COLOR: 90 | return text 91 | return "\033[{color};{style}m{message}\033[0m".format( 92 | style=style, color=color, message=text 93 | ) 94 | 95 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/diff.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | from qt_py_convert.color import ANSI, SUPPORTS_COLOR, color_text 23 | 24 | 25 | class Chunk(object): 26 | """ 27 | Chunk object maintains the split value and the key that was used to split 28 | it from the next Chunk object. 29 | """ 30 | def __init__(self, value, sep_method): 31 | super(Chunk, self).__init__() 32 | self.value = value 33 | self.sep = sep_method 34 | 35 | def __repr__(self): 36 | return "" % (self.value, self.sep) 37 | 38 | def __eq__(self, other): 39 | if isinstance(other, Chunk): 40 | return self.value == other.value 41 | return self.value == other 42 | 43 | def __ne__(self, other): 44 | return not self.__eq__(other) 45 | 46 | def __hash__(self): 47 | return hash(self.value) 48 | 49 | 50 | def chunk_str(msg, sep=(" ", ".", ",", "(")): 51 | """ 52 | chunk_str will take a string and a tuple of separators and recursively 53 | split the string into Chunk objects that record which separator was 54 | used to split it. 55 | :param msg: The string that we want to break apart. 56 | :type msg: str 57 | :param sep: Tuple of separators that you split on. 58 | :type sep: tuple[str...] 59 | :return: List of Chunk objects. 60 | :rtype: list[Chunk] 61 | """ 62 | parts = [(part, sep[0]) for part in msg.split(sep[0])] 63 | out = [] 64 | if len(sep) <= 1: 65 | return [Chunk(p[0], p[1]) for p in parts] 66 | for part, prev_split in parts: 67 | resulting_parts = chunk_str(part, sep=sep[1:]) 68 | if len(resulting_parts) == 1: 69 | out.append(Chunk(part, prev_split)) 70 | else: 71 | for resulting_chunk in resulting_parts[:-1]: 72 | out.append(resulting_chunk) 73 | out.append( 74 | Chunk(resulting_parts[-1].value, prev_split) 75 | ) 76 | # out.append((resulting_parts, prev_split)) 77 | out[-1].sep = "" 78 | return out 79 | 80 | 81 | def _match(first_list, second_list): 82 | output = [] 83 | for first_index, _chunk in enumerate(first_list): 84 | if _chunk in second_list: 85 | output.append([first_index, second_list.index(_chunk.value)]) 86 | return output 87 | 88 | 89 | def _equalize(first, second, sep=(" ", ".", ",", "(")): 90 | """ 91 | equalize will take the two strings and attempt to build to lists of 92 | Chunks where the list are of equal lengths. 93 | 94 | :param first: First string that you want to compare. 95 | :type first: str 96 | :param second: Second string that you want to compare. 97 | :type second: str 98 | :param sep: Tuple of separators that you split on. 99 | :type sep: tuple[str...] 100 | :return: Tuple of two lists of Chunks 101 | :rtype: tuple[list[Chunk]] 102 | """ 103 | first_chunk_list = chunk_str(first, sep=sep) 104 | second_chunk_list = chunk_str(second, sep=sep) 105 | matches = _match(first_chunk_list, second_chunk_list) 106 | if matches: 107 | if len(second_chunk_list) <= len(first_chunk_list): 108 | for first_loc, second_loc in matches: 109 | for _ in range(first_loc - second_loc): 110 | second_chunk_list.insert( 111 | second_loc - 1, Chunk("", "") 112 | ) 113 | while len(second_chunk_list) < len(first_chunk_list): 114 | second_chunk_list.append(Chunk("", "")) 115 | else: 116 | for first_loc, second_loc in matches: 117 | for _ in range(second_loc - first_loc): 118 | first_chunk_list.insert( 119 | first_loc - 1, Chunk("", "") 120 | ) 121 | while len(first_chunk_list) < len(second_chunk_list): 122 | first_chunk_list.append(Chunk("", "")) 123 | else: 124 | if not len(first_chunk_list) == len(second_chunk_list): 125 | if len(first_chunk_list) > len(second_chunk_list): 126 | larger = first_chunk_list 127 | smaller = second_chunk_list 128 | else: 129 | larger = second_chunk_list 130 | smaller = first_chunk_list 131 | 132 | for count in range(len(larger) - len(smaller)): 133 | smaller.append(Chunk("", "")) 134 | 135 | return first_chunk_list, second_chunk_list 136 | 137 | 138 | def highlight_diffs(first, second, sep=(" ", ".", ",", "(")): 139 | if not SUPPORTS_COLOR: 140 | return first, second 141 | first_chunks, second_chunks = _equalize(first, second, sep=sep) 142 | first_out = "" 143 | second_out = "" 144 | for first_chunk, second_chunk in zip(first_chunks, second_chunks): 145 | if first_chunk != second_chunk: 146 | if first_chunk.value: 147 | first_out += color_text( 148 | color=ANSI.colors.green, 149 | text=first_chunk.value, 150 | style=ANSI.styles.strong 151 | ) 152 | if first_chunk.sep: 153 | first_out += color_text( 154 | color=ANSI.colors.gray, 155 | text=first_chunk.sep, 156 | ) 157 | 158 | if second_chunk.value: 159 | second_out += color_text( 160 | color=ANSI.colors.green, 161 | text=second_chunk.value, 162 | style=ANSI.styles.strong 163 | ) 164 | if second_chunk.sep: 165 | second_out += color_text( 166 | color=ANSI.colors.gray, 167 | text=second_chunk.sep, 168 | ) 169 | else: 170 | if first_chunk.value: 171 | first_out += color_text( 172 | color=ANSI.colors.gray, 173 | text=first_chunk.value, 174 | ) 175 | if first_chunk.sep: 176 | first_out += color_text( 177 | color=ANSI.colors.gray, 178 | text=first_chunk.sep, 179 | ) 180 | 181 | if second_chunk.value: 182 | second_out += color_text( 183 | color=ANSI.colors.gray, 184 | text=second_chunk.value, 185 | ) 186 | if second_chunk.sep: 187 | second_out += color_text( 188 | color=ANSI.colors.gray, 189 | text=second_chunk.sep, 190 | ) 191 | return first_out, second_out 192 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/general.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | """ 23 | general is utility functions for the qt_py_convert library 24 | """ 25 | import copy 26 | import json 27 | import os 28 | import re 29 | 30 | import Qt 31 | 32 | from qt_py_convert.color import ANSI, color_text 33 | from qt_py_convert.diff import highlight_diffs 34 | from qt_py_convert.log import get_logger 35 | 36 | 37 | GENERAL_LOGGER = get_logger("general", name_color=ANSI.colors.green) 38 | 39 | 40 | class WriteFlag(object): 41 | WRITE_TO_FILE = 0b0001 42 | WRITE_TO_STDOUT = 0b0010 43 | 44 | 45 | def change(logger, node, replacement, skip_lineno=False, msg=None): 46 | """ 47 | A helper function to print information about replacing a node. 48 | 49 | :param logger: A python logger 50 | :type logger: logger.Logger 51 | :param node: Redbaron node that you are going to replace. 52 | :type node: redbaron.node 53 | :param replacement: Replacement string. 54 | :type replacement: str 55 | :param skip_lineno: Skip lineno flag. 56 | :type skip_lineno: bool 57 | :param msg: Optional custom message to write out. 58 | :type msg: None|str 59 | :return: Returns the result of the handler. 60 | :rtype: None 61 | """ 62 | failure_message = ( 63 | color_text(color=ANSI.colors.orange, text="WARNING:") + 64 | " Could not replace \"{original}\" with \"{replacement}\"" 65 | ) 66 | if msg is None: 67 | msg = "Replacing \"{original}\" with \"{replacement}\"" 68 | 69 | _orig = str(node).strip("\n") 70 | _repl = replacement 71 | original, replacement = highlight_diffs(_orig, _repl) 72 | if not skip_lineno: 73 | msg += " at line {line}" 74 | if not hasattr(node, "absolute_bounding_box"): 75 | msg = failure_message 76 | line = "N/A" 77 | else: 78 | line = node.absolute_bounding_box.top_left.line - 1 79 | 80 | result = logger.debug( 81 | msg.format(**locals()) 82 | ) 83 | if msg == failure_message: 84 | result = 1 85 | return result 86 | 87 | 88 | # Default binding support out of the box. 89 | __supported_bindings__ = ("PySide2", "PyQt5", "PySide", "PyQt4") 90 | __suplimentary_bindings__ = ["sip", "shiboken"] 91 | # Adding support for custom bindings. 92 | CUSTOM_MISPLACED_MEMBERS = "QT_CUSTOM_MISPLACED_MEMBERS" 93 | _custom_bindings = os.environ.get("QT_CUSTOM_BINDINGS_SUPPORT") 94 | if _custom_bindings: 95 | GENERAL_LOGGER.debug( 96 | "Found Custom Bindings. Adding: {bindings}".format( 97 | bindings=_custom_bindings.split(os.pathsep) 98 | ) 99 | ) 100 | __supported_bindings__ += _custom_bindings.split(os.pathsep) 101 | 102 | # Note: Pattern here is a little more complex than needed to make the 103 | # print lines optional. 104 | _custom_misplaced_members = {} 105 | misplaced_members_python_str = os.environ.get(CUSTOM_MISPLACED_MEMBERS) 106 | if misplaced_members_python_str: 107 | GENERAL_LOGGER.debug( 108 | "{} = {0!r}".format( 109 | CUSTOM_MISPLACED_MEMBERS, misplaced_members_python_str 110 | ) 111 | ) 112 | 113 | _custom_misplaced_members = json.loads(misplaced_members_python_str) 114 | 115 | # Colored green 116 | GENERAL_LOGGER.debug(color_text( 117 | text="Resolved {} to json: {0!r}".format( 118 | CUSTOM_MISPLACED_MEMBERS, _custom_misplaced_members 119 | ), 120 | color=ANSI.colors.green 121 | )) 122 | 123 | 124 | class ErrorClass(object): 125 | """ 126 | ErrorClass is a structured data block that represents a problem with the 127 | converted file that cannot be automatically fixed from qy_py_convert. 128 | 129 | It takes a redbaron node and a str to describe why it can't be fixed. 130 | """ 131 | def __init__(self, row_from, row_to, reason): 132 | """ 133 | :param node: Redbaron node that can't be fixed. 134 | :type node: redbaron.Node 135 | :param reason: Reason that the thing cannot be fixed. 136 | :type reason: str 137 | """ 138 | super(ErrorClass, self).__init__() 139 | 140 | self.row = row_from 141 | self.row_to = row_to 142 | self.reason = reason 143 | ALIAS_DICT["errors"].add(self) 144 | 145 | @classmethod 146 | def from_node(cls, node, reason): 147 | bbox = node.absolute_bounding_box 148 | 149 | row = bbox.top_left.line - 1 150 | row_to = bbox.bottom_right.line - 1 151 | reason = reason 152 | return cls(row_from=row, row_to=row_to, reason=reason) 153 | 154 | 155 | class UserInputRequiredException(BaseException): 156 | """ 157 | UserInputRequiredException is an exception that states that the user is 158 | required to make the fix. It is used to alert the user to issues. 159 | """ 160 | 161 | 162 | class AliasDictClass(dict): 163 | """ 164 | Global state data store 165 | """ 166 | BINDINGS = "bindings" 167 | ALIASES = "root_aliases" 168 | USED = "used" 169 | WARNINGS = "warnings" 170 | ERRORS = "errors" 171 | 172 | def __init__(self): 173 | super(AliasDictClass, self).__init__( 174 | dict([ 175 | (self.BINDINGS, set()), 176 | (self.ALIASES, set()), 177 | (self.USED, set()), 178 | (self.WARNINGS, set()), 179 | (self.ERRORS, set()), 180 | ]) 181 | ) 182 | 183 | def clean(self): 184 | """clean will reset the AliasDict global object.""" 185 | GENERAL_LOGGER.debug(color_text( 186 | text="Cleaning the global AliasDict", 187 | color=ANSI.colors.red 188 | )) 189 | self[self.BINDINGS] = set() 190 | self[self.ALIASES] = set() 191 | self[self.USED] = set() 192 | self[self.WARNINGS] = set() 193 | self[self.ERRORS] = set() 194 | 195 | 196 | ALIAS_DICT = AliasDictClass() 197 | 198 | 199 | def merge_dict(lhs, rhs, keys=None, keys_both=False): 200 | """ 201 | Basic merge dictionary function. I assume it works, I haven't looked at 202 | it for eons. 203 | 204 | :param lhs: Left dictionary. 205 | :type lhs: dict 206 | :param rhs: Right dictionary. 207 | :type rhs: dict 208 | :param keys: Keys to merge. 209 | :type keys: None|List[str...] 210 | :param keys_both: Use the union of the keys from both? 211 | :type keys_both: bool 212 | :return: Merged dictionary. 213 | :rtype: dict 214 | """ 215 | out = {} 216 | lhs = copy.copy(lhs) 217 | rhs = copy.copy(rhs) 218 | if not keys: 219 | keys = list(lhs.keys()) 220 | if keys_both: 221 | keys.extend(list(rhs.keys())) 222 | for key in keys: 223 | if key not in rhs: 224 | rhs[key] = type(lhs[key])() 225 | if key not in lhs: 226 | lhs[key] = type(rhs[key])() 227 | if isinstance(lhs[key], set): 228 | op = "union" 229 | elif isinstance(lhs[key], str): 230 | op = "__add__" 231 | else: 232 | op = None 233 | if op: 234 | out[key] = getattr(lhs[key], op)(rhs[key]) 235 | # out[key] = lhs[key].union(rhs[key]) 236 | return out 237 | 238 | 239 | def supported_binding(binding_str): 240 | bindings_by_length = [ 241 | re.escape(binding) 242 | for binding in sorted(__supported_bindings__, reverse=True) 243 | ] 244 | match = re.match( 245 | r"^(?P{bindings})(:?\..*)?".format( 246 | bindings="|".join(bindings_by_length) 247 | ), 248 | binding_str 249 | ) 250 | if match: 251 | return match.groupdict().get("binding") 252 | return None 253 | 254 | 255 | def is_py(path): 256 | """ 257 | My helper method for process_folder to decide if a file is a python file 258 | or not. 259 | It is currently checking the file extension and then falling back to 260 | checking the first line of the file. 261 | 262 | :param path: The filepath to the file that we are querying. 263 | :type path: str 264 | :return: True if it's a python file. False otherwise 265 | :rtype: bool 266 | """ 267 | if path.endswith(".py"): 268 | return True 269 | elif not os.path.splitext(path)[1] and os.path.isfile(path): 270 | with open(path, "rb") as fh: 271 | if "python" in fh.readline(): 272 | return True 273 | return False 274 | 275 | 276 | def build_exc(error, line_data): 277 | """ 278 | raises a UserInputRequiredException from an instance of an ErrorClass. 279 | 280 | :param error: The ErrorClass instance that was created somewhere in 281 | qt_py_convert. 282 | :type error: qt_py_convert.general.ErrorClass 283 | :param line_data: List of lines from the file we are working on. 284 | :type line_data: List[str...] 285 | """ 286 | line_no_start = error.row 287 | line_no_end = error.row_to + 1 288 | lines = line_data[line_no_start:line_no_end] 289 | line = "".join(line_data[line_no_start:line_no_end]) 290 | 291 | line_no = "Line" 292 | if len(lines) > 1: 293 | line_no += "s " 294 | line_no += "%d-%d" % (line_no_start + 1, line_no_end) 295 | else: 296 | line_no += " %d" % (line_no_start + 1) 297 | 298 | template = """ 299 | {line_no} 300 | {line} 301 | {reason} 302 | """ 303 | raise UserInputRequiredException(color_text( 304 | text=template.format( 305 | line_no=line_no, 306 | line=color_text(text=line.rstrip("\n"), color=ANSI.colors.gray), 307 | reason=color_text(text=error.reason, color=ANSI.colors.red), 308 | ), 309 | color=ANSI.colors.red, 310 | )) 311 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/log.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | import logging 23 | import os 24 | import sys 25 | 26 | from qt_py_convert.color import ANSI, color_text 27 | 28 | __BASE_LOGGING_NAME = "QtPyConvert" 29 | 30 | 31 | class ColoredFormatter(logging.Formatter): 32 | COLORS = { 33 | 'WARNING': ANSI.colors.orange, 34 | 'INFO': ANSI.colors.white, 35 | 'DEBUG': ANSI.colors.blue, 36 | 'CRITICAL': ANSI.colors.red, 37 | 'ERROR': ANSI.colors.red 38 | } 39 | 40 | def __init__(self, fmt, dt=None): 41 | logging.Formatter.__init__(self, fmt, dt) 42 | 43 | def format(self, record): 44 | levelname = record.levelname 45 | if levelname in self.COLORS: 46 | levelname_color = color_text( 47 | text=levelname, 48 | color=self.COLORS[levelname], 49 | style=ANSI.styles.strong 50 | ) 51 | record.levelname = levelname_color 52 | return logging.Formatter.format(self, record) 53 | 54 | 55 | def get_formatter(name="%(name)s", name_color=ANSI.colors.purple, 56 | name_style=ANSI.styles.plain, msg_color=ANSI.colors.white): 57 | 58 | custom_name = color_text(text=name, color=name_color, style=name_style) 59 | message = color_text(text="%(message)s", color=msg_color) 60 | formatter = ColoredFormatter( 61 | "%(asctime)s - %(levelname)s | [" + custom_name + "] " + message, 62 | "%Y-%m-%d %H:%M:%S" 63 | ) 64 | return formatter 65 | 66 | 67 | def get_logger(logger_name, level=None, 68 | name_color=ANSI.colors.purple, message_color=ANSI.colors.white): 69 | 70 | if not logger_name.startswith(__BASE_LOGGING_NAME): 71 | logger_name = __BASE_LOGGING_NAME + "." + logger_name 72 | 73 | if level is None: 74 | level = os.environ.get("LOGLEVEL", logging.INFO) 75 | 76 | logger = logging.getLogger(logger_name) 77 | handler = logging.StreamHandler(sys.stdout) 78 | formatter = get_formatter(name_color=name_color, msg_color=message_color) 79 | handler.setFormatter(formatter) 80 | logger.addHandler(handler) 81 | handler.setLevel(level) 82 | logger.setLevel(level) 83 | return logger 84 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/mappings.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | import re 23 | 24 | import Qt 25 | 26 | from qt_py_convert.log import get_logger 27 | from qt_py_convert.general import _custom_misplaced_members 28 | 29 | MAPPINGS_LOG = get_logger("mappings") 30 | 31 | 32 | def misplaced_members(aliases, mappings): 33 | """ 34 | misplaced_members uses the internal "_misplaced_members" from Qt.py as 35 | well as any "_custom_misplaced_members" that you have set to update the 36 | detected binding members. The Qt.py misplaced members aid in updating 37 | bindings to Qt5 compatible locations. 38 | 39 | :param aliases: Aliases is the replacement information that is build 40 | automatically from qt_py_convert. 41 | :type aliases: dict 42 | :param mappings: Mappings is information about the bindings that are used. 43 | :type mappings: dict 44 | :return: A tuple of aliases and mappings that have been updated. 45 | :rtype: tuple[dict,dict] 46 | """ 47 | members = Qt._misplaced_members.get(Qt.__binding__.lower(), {}) 48 | for binding in aliases["bindings"]: 49 | if binding in Qt._misplaced_members: 50 | MAPPINGS_LOG.debug("Merging {misplaced} to bindings".format( 51 | misplaced=Qt._misplaced_members.get(binding, {}) 52 | )) 53 | members.update(Qt._misplaced_members.get(binding, {})) 54 | elif binding in _custom_misplaced_members: 55 | members.update(_custom_misplaced_members.get(binding, {})) 56 | else: 57 | MAPPINGS_LOG.debug( 58 | "Could not find misplaced members for {}".format(binding) 59 | ) 60 | 61 | _msg = "Replacing \"{original}\" with \"{replacement}\" in mappings" 62 | if members: 63 | for source in members: 64 | replaced = False 65 | dest = members[source] 66 | if isinstance(dest, (list, tuple)): 67 | dest, _ = members[source] 68 | for current_key in mappings: 69 | if mappings[current_key] == source: 70 | MAPPINGS_LOG.debug( 71 | _msg.format( 72 | original=mappings[current_key], 73 | replacement=dest 74 | ) 75 | ) 76 | mappings[current_key] = dest 77 | replaced = True 78 | if not replaced: 79 | MAPPINGS_LOG.debug( 80 | "Adding {bind} in mappings".format(bind=dest) 81 | ) 82 | mappings[source] = dest 83 | return aliases, mappings 84 | 85 | 86 | def convert_mappings(aliases, mappings): 87 | """ 88 | convert_mappings will build a proper mapping dictionary using any 89 | aliases that we have discovered previously. 90 | It builds regular expressions based off of the Qt._common_members and will 91 | replace the mappings that are used with updated ones in Qt.py 92 | 93 | :param aliases: Aliases is the replacement information that is build 94 | automatically from qt_py_convert. 95 | :type aliases: dict 96 | :param mappings: Mappings is information about the bindings that are used. 97 | :type mappings: dict 98 | :return: _convert_mappings will just return the mappings dict, 99 | however it is updating the aliases["used"] set. 100 | :rtype: dict 101 | """ 102 | expressions = [ 103 | re.compile( 104 | r"(?P{modules})\.(?P{widgets})$".format( 105 | # Regular expression 106 | modules="|".join( 107 | re.escape(name) for name in Qt._common_members.keys() 108 | ), 109 | widgets="|".join( 110 | re.escape(widget) for widget in Qt._common_members[module] 111 | ) 112 | ) 113 | ) 114 | for module in Qt._common_members.keys() 115 | ] 116 | for from_mapping in mappings: 117 | iterable = zip(Qt._common_members.keys(), expressions) 118 | for module_name, expression in iterable: 119 | modified_mapping = expression.sub( 120 | r"{module}.\2".format(module=module_name), 121 | mappings[from_mapping] 122 | ) 123 | if modified_mapping != mappings[from_mapping]: 124 | # Mapping changed 125 | # _---------------------------_ # 126 | # We shouldn't be adding it here. 127 | # We don't know if it's used yet. 128 | # aliases["used"].add(module_name) 129 | mappings[from_mapping] = modified_mapping 130 | return mappings 131 | -------------------------------------------------------------------------------- /src/python/qt_py_convert/run.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Digital Domain 3.0 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "Apache License") 4 | # with the following modification; you may not use this file except in 5 | # compliance with the Apache License and the following modification to it: 6 | # Section 6. Trademarks. is deleted and replaced with: 7 | # 8 | # 6. Trademarks. This License does not grant permission to use the trade 9 | # names, trademarks, service marks, or product names of the Licensor 10 | # and its affiliates, except as required to comply with Section 4(c) of 11 | # the License and to reproduce the content of the NOTICE file. 12 | # 13 | # You may obtain a copy of the Apache License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the Apache License with the above modification is 19 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 | # KIND, either express or implied. See the Apache License for the specific 21 | # language governing permissions and limitations under the Apache License. 22 | import os 23 | import re 24 | import sys 25 | import traceback 26 | 27 | 28 | import Qt 29 | if Qt.__version__ < "1.2.0.b2": 30 | raise ImportError( 31 | "Improper Qt.py version installed. Qt.py must be version 1.2.0.b2 or above. Version %s installed instead." % Qt.__version__ 32 | ) 33 | import redbaron 34 | 35 | from qt_py_convert._modules import from_imports 36 | from qt_py_convert._modules import imports 37 | from qt_py_convert._modules import psep0101 38 | from qt_py_convert._modules import unsupported 39 | from qt_py_convert.general import merge_dict, ErrorClass, \ 40 | ALIAS_DICT, change, UserInputRequiredException, ANSI, \ 41 | __suplimentary_bindings__, is_py, build_exc, WriteFlag 42 | from qt_py_convert.color import color_text 43 | from qt_py_convert.mappings import convert_mappings, misplaced_members 44 | from qt_py_convert.log import get_logger 45 | 46 | COMMON_MODULES = list(Qt._common_members.keys()) + ["QtCompat"] 47 | 48 | 49 | MAIN_LOG = get_logger("run") 50 | Qt4_Qt5_LOG = get_logger("qt4->qt5") 51 | 52 | 53 | def _cleanup_imports(red, aliases, mappings, skip_lineno=False): 54 | """ 55 | _cleanup_imports fixes the imports. 56 | Initially changing them as per the following: 57 | >>> from PyQt4 import QtGui, QtCore 58 | to 59 | >>> from Qt import QtGui, QtCore 60 | for each binding. 61 | It doesn't have enough knowledge of your script at this point to know if 62 | you need QtWidgets or if the ones you import are all used. 63 | This will get reflected at the end. 64 | 65 | :param red: The redbaron ast. 66 | :type red: redbaron.RedBaron 67 | :param aliases: Aliases is the replacement information that is build 68 | automatically from qt_py_convert. 69 | :type aliases: dict 70 | :param mappings: Mappings is information about the bindings that are used. 71 | :type mappings: dict 72 | :param skip_lineno: An optional performance flag. By default, when the 73 | script replaces something, it will tell you which line it is 74 | replacing on. This can be useful for tracking the places that 75 | changes occurred. When you turn this flag on however, it will not 76 | show the line numbers. This can give great performance increases 77 | because redbaron has trouble calculating the line number sometimes. 78 | :type skip_lineno: bool 79 | """ 80 | replaced = False 81 | deletion_index = [] 82 | imps = red.find_all("FromImportNode") 83 | imps += red.find_all("ImportNode") 84 | 85 | MAIN_LOG.debug(color_text( 86 | text="===========================", 87 | color=ANSI.colors.blue 88 | )) 89 | MAIN_LOG.debug(color_text( 90 | text="Consolidating Import lines.", 91 | color=ANSI.colors.blue, 92 | style=ANSI.styles.underline, 93 | )) 94 | 95 | for child in imps: 96 | for value in child.value: 97 | value_str = value.value 98 | try: 99 | value_str = value_str.dumps() 100 | except AttributeError: 101 | pass 102 | if value.value == "Qt" or value_str in __suplimentary_bindings__: 103 | if not replaced: 104 | names = filter( 105 | lambda a: True if a in COMMON_MODULES else False, 106 | aliases["used"], 107 | ) 108 | if not names: # Attempt to build names from input aliases. 109 | members = filter( 110 | lambda a: True if a in mappings else False, 111 | aliases["root_aliases"], 112 | ) 113 | names = [] 114 | for member in members: 115 | names.append(mappings[member].split(".")[0]) 116 | 117 | if not names: 118 | MAIN_LOG.warning(color_text( 119 | text="We have found no usages of Qt in " 120 | "this script despite you previously" 121 | " having imported the binding.\nIf " 122 | "you think this is in error, " 123 | "please let us know and submit an " 124 | "issue ticket with the example you " 125 | "think is wrong.", 126 | color=ANSI.colors.green 127 | )) 128 | child.parent.remove(child) 129 | continue 130 | # What we want to replace to. 131 | replace_text = "from Qt import {key}".format( 132 | key=", ".join(names) 133 | ) 134 | 135 | cleaning_message = color_text( 136 | text="Cleaning", color=ANSI.colors.green 137 | ) 138 | cleaning_message += ( 139 | " imports from: \"{original}\" to \"{replacement}\"" 140 | ) 141 | change( 142 | msg=cleaning_message, 143 | logger=MAIN_LOG, 144 | node=child, 145 | replacement=replace_text, 146 | skip_lineno=skip_lineno 147 | ) 148 | 149 | child.replace(replace_text) 150 | replaced = True 151 | else: 152 | deleting_message = "{name} \"{orig}\"".format( 153 | orig=str(child).strip("\n"), 154 | name=color_text(text="Deleting", color=ANSI.colors.red) 155 | ) 156 | change( 157 | msg=deleting_message, 158 | logger=MAIN_LOG, 159 | node=child, 160 | replacement="", 161 | skip_lineno=skip_lineno 162 | ) 163 | child.parent.remove(child) 164 | else: 165 | pass 166 | for child in reversed(deletion_index): 167 | MAIN_LOG.debug("Deleting {node}".format(node=child)) 168 | child.parent.remove(child) 169 | # red.remove(child) 170 | 171 | 172 | def _convert_attributes(red, aliases, skip_lineno=False): 173 | """ 174 | _convert_attributes converts all AtomTrailersNodes and DottenNameNodes to 175 | the Qt5/PySide2 api matching Qt.py.. 176 | This means that anything that was using QtGui but is now using QtWidgets 177 | will be updated for example. 178 | It does not do any api v1 - api v2 conversion or specific 179 | misplaced_mapping changes. 180 | 181 | :param red: The redbaron ast. 182 | :type red: redbaron.RedBaron 183 | :param aliases: Aliases is the replacement information that is build 184 | automatically from qt_py_convert. 185 | :type aliases: dict 186 | :param skip_lineno: An optional performance flag. By default, when the 187 | script replaces something, it will tell you which line it is 188 | replacing on. This can be useful for tracking the places that 189 | changes occurred. When you turn this flag on however, it will not 190 | show the line numbers. This can give great performance increases 191 | because redbaron has trouble calculating the line number sometimes. 192 | :type skip_lineno: bool 193 | """ 194 | # Compile our expressions 195 | # Our expressions are basically as follows: 196 | # From: 197 | # . 198 | # To: 199 | # .<\back reference to the member matched> 200 | # Where A is the specific Qt SecondLevelModule that we are building this 201 | # expression for. 202 | # 203 | # Also sorry this is longer than 79 chars.. 204 | # It gets harder to read the more I try to make it more readable. 205 | expressions = [ 206 | ( 207 | re.compile( 208 | r"^(?P{modules})\.(?P(?:{widgets})(?:[.\[(].*)?)$".format( # Regular expression 209 | modules="|".join(re.escape(name) for name in Qt._common_members.keys()), 210 | widgets="|".join(re.escape(widget) for widget in Qt._common_members[module_name]) 211 | ), 212 | re.MULTILINE 213 | ), 214 | module_name 215 | ) 216 | for module_name in Qt._common_members 217 | ] 218 | 219 | def finder_function_factory(exprs): 220 | """Basic function factory. Used as a find_all delegate for red.""" 221 | def finder_function(value): 222 | """The filter for our red.find_all function.""" 223 | return any([ 224 | expression.match(value.dumps()) for expression, mod in exprs 225 | ]) 226 | return finder_function 227 | 228 | mappings = {} 229 | # Find any AtomTrailersNode that matches any of our expressions. 230 | nodes = red.find_all( 231 | "AtomTrailersNode", 232 | value=finder_function_factory(expressions) 233 | ) 234 | nodes += red.find_all( 235 | "DottedNameNode", 236 | value=finder_function_factory(expressions) 237 | ) 238 | header_written = False 239 | for node in nodes: 240 | orig_node_str = node.dumps() 241 | added_module = False 242 | for expr, module_ in expressions: 243 | modified = expr.sub( 244 | r"{module}.\2".format(module=module_), 245 | orig_node_str, 246 | ) 247 | 248 | if modified != orig_node_str: 249 | mappings[orig_node_str] = modified 250 | aliases["used"].add(module_) 251 | added_module = True 252 | if not header_written: 253 | MAIN_LOG.debug(color_text( 254 | text="=========================", 255 | color=ANSI.colors.orange, 256 | )) 257 | MAIN_LOG.debug(color_text( 258 | text="Parsing AtomTrailersNodes", 259 | color=ANSI.colors.orange, 260 | style=ANSI.styles.underline 261 | )) 262 | header_written = True 263 | 264 | repl = str(node).replace( 265 | str(node.value[0]).strip("\n"), 266 | module_ 267 | ) 268 | 269 | change( 270 | logger=Qt4_Qt5_LOG, 271 | node=node, 272 | replacement=repl, 273 | skip_lineno=skip_lineno 274 | ) 275 | # Only replace the first node part of the statement. 276 | # This allows us to keep any child nodes that have already 277 | # been gathered attached to the main node tree. 278 | 279 | # This was the cause of a bug in our internal code. 280 | # http://dd-git.d2.com/ahughes/qt_py_convert/issues/19 281 | 282 | # A node that had child nodes that needed replacements on the 283 | # same line would cause an issue if we replaced the entire 284 | # line the first replacement. The other replacements on that 285 | # line would not stick because they would be replacing to an 286 | # orphaned tree. 287 | node.value[0].replace(module_) 288 | break 289 | # else: 290 | # if orig_node_str.split(".")[0] in COMMON_MODULES: 291 | # aliases["used"].add(orig_node_str.split(".")[0]) 292 | if not added_module: 293 | aliases["used"].add(orig_node_str.split(".")[0]) 294 | return mappings 295 | 296 | 297 | def _convert_root_name_imports(red, aliases, skip_lineno=False): 298 | """ 299 | _convert_root_name_imports is a function that should be used in cases 300 | where the original code just imported the python binding and did not 301 | import any second level modules. 302 | 303 | For example: 304 | ``` 305 | import PySide 306 | 307 | ``` 308 | :param red: The redbaron ast. 309 | :type red: redbaron.RedBaron 310 | :param aliases: Aliases is the replacement information that is build 311 | automatically from qt_py_convert. 312 | :type aliases: dict 313 | :param skip_lineno: An optional performance flag. By default, when the 314 | script replaces something, it will tell you which line it is 315 | replacing on. This can be useful for tracking the places that 316 | changes occurred. When you turn this flag on however, it will not 317 | show the line numbers. This can give great performance increases 318 | because redbaron has trouble calculating the line number sometimes. 319 | :type skip_lineno: bool 320 | """ 321 | def filter_function(value): 322 | """A filter delegate for our red.find_all function.""" 323 | return value.dumps().startswith("Qt.") 324 | matches = red.find_all("AtomTrailersNode", value=filter_function) 325 | matches += red.find_all("DottedNameNode", value=filter_function) 326 | lstrip_qt_regex = re.compile(r"^Qt\.",) 327 | 328 | if matches: 329 | MAIN_LOG.debug(color_text( 330 | text="====================================", 331 | color=ANSI.colors.purple, 332 | )) 333 | MAIN_LOG.debug(color_text( 334 | text="Replacing top level binding imports.", 335 | color=ANSI.colors.purple, 336 | style=ANSI.styles.underline, 337 | )) 338 | 339 | for node in matches: 340 | name = lstrip_qt_regex.sub( 341 | "", node.dumps(), count=1 342 | ) 343 | 344 | root_name = name.split(".")[0] 345 | if root_name in COMMON_MODULES: 346 | aliases["root_aliases"].add( 347 | root_name 348 | ) 349 | change( 350 | logger=MAIN_LOG, 351 | node=node, 352 | replacement=name, 353 | skip_lineno=skip_lineno 354 | ) 355 | node.replace(name) 356 | else: 357 | MAIN_LOG.warning( 358 | "Unknown second level module from the Qt package \"{}\"" 359 | .format( 360 | color_text(text=root_name, color=ANSI.colors.orange) 361 | ) 362 | ) 363 | 364 | 365 | def _convert_body(red, aliases, mappings, skip_lineno=False): 366 | """ 367 | _convert_body is one of the first conversion functions to run on the 368 | redbaron ast. 369 | It finds the NameNode's or the AtomTrailersNode+DottedNameNodes and will 370 | run them through the filter expressions built off of the values in 371 | mappings. 372 | If found, it will replace the source value with the destination value in 373 | mappings. 374 | 375 | :param red: The redbaron ast. 376 | :type red: redbaron.RedBaron 377 | :param aliases: Aliases is the replacement information that is build 378 | automatically from qt_py_convert. 379 | :type aliases: dict 380 | :param mappings: Mappings is information about the bindings that are used. 381 | :type mappings: dict 382 | :param skip_lineno: An optional performance flag. By default, when the 383 | script replaces something, it will tell you which line it is 384 | replacing on. This can be useful for tracking the places that 385 | changes occurred. When you turn this flag on however, it will not 386 | show the line numbers. This can give great performance increases 387 | because redbaron has trouble calculating the line number sometimes. 388 | :type skip_lineno: bool 389 | """ 390 | def expression_factory(expr_key): 391 | """ 392 | expression_factory is a function factory for building a regex.match 393 | function for a specific key that we found in misplaced_mappings 394 | """ 395 | regex = re.compile( 396 | r"{value}(?:[\.\[\(].*)?$".format(value=expr_key), 397 | re.DOTALL 398 | ) 399 | 400 | def expression_filter(value): 401 | """ 402 | Basic filter function matching for red.find_all against a regex 403 | previously created from the factory 404 | .""" 405 | return regex.match(value.dumps()) 406 | 407 | return expression_filter 408 | 409 | # Body of the function 410 | for key in sorted(mappings, key=len): 411 | MAIN_LOG.debug(color_text( 412 | text="-"*len(key), 413 | color=ANSI.colors.teal, 414 | )) 415 | MAIN_LOG.debug(color_text( 416 | text=key, 417 | color=ANSI.colors.teal, 418 | style=ANSI.styles.underline, 419 | )) 420 | if "." in key: 421 | filter_function = expression_factory(key) 422 | matches = red.find_all("AtomTrailersNode", value=filter_function) 423 | matches += red.find_all("DottedNameNode", value=filter_function) 424 | else: 425 | matches = red.find_all("NameNode", value=key) 426 | if matches: 427 | for node in matches: 428 | # Dont replace imports, we already did that. 429 | parent_is_import = node.parent_find("ImportNode") 430 | parent_is_fimport = node.parent_find("FromImportNode") 431 | if not parent_is_import and not parent_is_fimport: 432 | # If the node's parent has dot syntax. Make sure we are 433 | # the first one. Reasoning: We are relying on namespacing, 434 | # so we don't want to turn bob.foo.cat into bob.foo.bear. 435 | # Because bob.foo.cat might not be equal to the mike.cat 436 | # that we meant to change. 437 | if hasattr(node.parent, "type") and node.parent.type == "atomtrailers": 438 | if not node.parent.value[0] == node: 439 | continue 440 | 441 | if key != mappings[key]: 442 | replacement = node.dumps().replace(key, mappings[key]) 443 | change( 444 | logger=MAIN_LOG, 445 | node=node, 446 | replacement=replacement, 447 | skip_lineno=skip_lineno 448 | ) 449 | if mappings[key].split(".")[0] in COMMON_MODULES: 450 | aliases["used"].add(mappings[key].split(".")[0]) 451 | 452 | node.replace(replacement) 453 | else: 454 | if node.dumps().split(".")[0] in COMMON_MODULES: 455 | aliases["used"].add(node.dumps().split(".")[0]) 456 | # match.replace(mappings[key]) 457 | 458 | 459 | def run(text, skip_lineno=False, tometh_flag=False, explicit_signals_flag=False): 460 | """ 461 | run is the main driver of the file. It takes the text of a file and any 462 | flags that you want to set. 463 | It does not deal with any file opening or writting, you must have teh raw 464 | text already. 465 | 466 | :param text: Text from a python file that you want to process. 467 | :type text: str 468 | :param skip_lineno: An optional performance flag. By default, when the 469 | script replaces something, it will tell you which line it is 470 | replacing on. This can be useful for tracking the places that 471 | changes occurred. When you turn this flag on however, it will not 472 | show the line numbers. This can give great performance increases 473 | because redbaron has trouble calculating the line number sometimes. 474 | :type skip_lineno: bool 475 | :param tometh_flag: tometh_flag is an optional feature flag. Once turned 476 | on, it will attempt to replace any QString/QVariant/etc apiv1.0 methods 477 | that are being used in your script. It is currently not smart enough to 478 | confirm that you don't have any custom objects with the same method 479 | signature to PyQt4's apiv1.0 ones. 480 | :type tometh_flag: bool 481 | :return: run will return a tuple of runtime information. aliases, 482 | mappings, and the resulting text. Aliases is the replacement 483 | information that it built, mappings is information about the bindings 484 | that were used. 485 | :rtype: tuple[dict,dict,str] 486 | """ 487 | ALIAS_DICT.clean() 488 | try: 489 | red = redbaron.RedBaron(text) 490 | except Exception as err: 491 | MAIN_LOG.critical(str(err)) 492 | traceback.print_exc() 493 | 494 | ErrorClass(row_from=0, row_to=0, reason=traceback.format_exc()) 495 | return ALIAS_DICT, {}, text 496 | 497 | from_a, from_m = from_imports.process(red, skip_lineno=skip_lineno) 498 | import_a, import_m = imports.process(red, skip_lineno=skip_lineno) 499 | mappings = merge_dict(from_m, import_m, keys_both=True) 500 | aliases = merge_dict(from_a, import_a, keys=["bindings", "root_aliases"]) 501 | 502 | aliases, mappings = misplaced_members(aliases, mappings) 503 | aliases["used"] = set() 504 | 505 | mappings = convert_mappings(aliases, mappings) 506 | 507 | # Convert using the psep0101 module. 508 | psep0101.process( 509 | red, 510 | skip_lineno=skip_lineno, 511 | tometh_flag=tometh_flag, 512 | explicit_signals_flag=explicit_signals_flag 513 | ) 514 | _convert_body(red, aliases, mappings, skip_lineno=skip_lineno) 515 | _convert_root_name_imports(red, aliases, skip_lineno=skip_lineno) 516 | _convert_attributes(red, aliases, skip_lineno=skip_lineno) 517 | if aliases["root_aliases"]: 518 | _cleanup_imports(red, aliases, mappings, skip_lineno=skip_lineno) 519 | 520 | # Build errors from our unsupported module. 521 | unsupported.process(red, skip_lineno=skip_lineno) 522 | 523 | # Done! 524 | dumps = red.dumps() 525 | return aliases, mappings, dumps 526 | 527 | 528 | def process_file(fp, write_mode=None, path=None, backup=False, skip_lineno=False, tometh_flag=False, explicit_signals_flag=False): 529 | """ 530 | One of the entry-point functions in qt_py_convert. 531 | If you are looking to process a single python file, this is your function. 532 | 533 | :param fp: The source file that you want to start processing. 534 | :type fp: str 535 | :param write_mode: The type of writing that we are doing. 536 | :type write_mode: int 537 | :param path: If passed, it will signify that we are not overwriting. 538 | It will be a tuple of (src_root, dst_roo) 539 | :type path: tuple[str,str] 540 | :param backup: If passed we will create a ".bak" file beside the newly 541 | created file. The .bak will contain the original source code. 542 | :type path: bool 543 | :param skip_lineno: An optional performance flag. By default, when the 544 | script replaces something, it will tell you which line it is 545 | replacing on. This can be useful for tracking the places that 546 | changes occurred. When you turn this flag on however, it will not 547 | show the line numbers. This can give great performance increases 548 | because redbaron has trouble calculating the line number sometimes. 549 | :type skip_lineno: bool 550 | :param tometh_flag: tometh_flag is an optional feature flag. Once turned 551 | on, it will attempt to replace any QString/QVariant/etc apiv1.0 methods 552 | that are being used in your script. It is currently not smart enough to 553 | confirm that you don't have any custom objects with the same method 554 | signature to PyQt4's apiv1.0 ones. 555 | :type tometh_flag: bool 556 | """ 557 | if not is_py(fp): 558 | MAIN_LOG.debug( 559 | "\tSkipping \"{fp}\"... It does not appear to be a python file." 560 | .format(fp=fp) 561 | ) 562 | return 563 | with open(fp, "rb") as fh: 564 | lines = fh.readlines() 565 | source = "".join(lines) 566 | 567 | MAIN_LOG.info("{line}\nProcessing {path}".format(path=fp, line="-"*50)) 568 | try: 569 | aliases, mappings, modified_code = run( 570 | source, 571 | skip_lineno=skip_lineno, 572 | tometh_flag=tometh_flag, 573 | explicit_signals_flag=explicit_signals_flag 574 | ) 575 | if aliases["used"] or modified_code != source: 576 | write_path = fp 577 | if write_mode & WriteFlag.WRITE_TO_STDOUT: 578 | sys.stdout.write(modified_code) 579 | else: 580 | if path and path[0]: # We are writing elsewhere than the source. 581 | src_root, dst_root = path 582 | root_relative = fp.replace(src_root, "").lstrip("/") 583 | write_path = os.path.join(dst_root, root_relative) 584 | 585 | if backup: # We are creating a source backup beside the output 586 | bak_path = os.path.join( 587 | os.path.dirname(write_path), 588 | "." + os.path.basename(write_path) + ".bak" 589 | ) 590 | MAIN_LOG.info("Backing up original code to {path}".format( 591 | path=bak_path 592 | )) 593 | with open(bak_path, "wb") as fh: 594 | fh.write(source) 595 | 596 | # Write to file. If path is None, we are overwriting. 597 | MAIN_LOG.info("Writing modifed code to {path}".format( 598 | path=write_path) 599 | ) 600 | 601 | if not os.path.exists(os.path.dirname(write_path)): 602 | os.makedirs(os.path.dirname(write_path)) 603 | with open(write_path, "wb") as fh: 604 | fh.write(modified_code) 605 | 606 | except BaseException: 607 | MAIN_LOG.critical("Error processing file: \"{path}\"".format(path=fp)) 608 | traceback.print_exc() 609 | 610 | # Process any errors that may have happened throughout the process. 611 | if ALIAS_DICT["errors"]: 612 | MAIN_LOG.error(color_text( 613 | text="The following errors were recovered from {}:\n".format(fp), 614 | color=ANSI.colors.red, 615 | )) 616 | for error in ALIAS_DICT["errors"]: 617 | try: 618 | build_exc(error, lines) 619 | except UserInputRequiredException as err: 620 | MAIN_LOG.error(str(err)) 621 | 622 | 623 | def process_folder(folder, recursive=False, write_mode=None, path=None, backup=False, skip_lineno=False, tometh_flag=False, explicit_signals_flag=False): 624 | """ 625 | One of the entry-point functions in qt_py_convert. 626 | If you are looking to process every python file in a folder, this is your 627 | function. 628 | 629 | :param folder: The source folder that you want to start processing the 630 | python files of. 631 | :type folder: str 632 | :param recursive: Do you want to continue recursing through sub-folders? 633 | :type recursive: bool 634 | :param write_mode: The type of writing that we are doing. 635 | :type write_mode: int 636 | :param path: If passed, it will signify that we are not overwriting. 637 | It will be a tuple of (src_root, dst_roo) 638 | :type path: tuple[str,str] 639 | :param backup: If passed we will create a ".bak" file beside the newly 640 | created file. The .bak will contain the original source code. 641 | :type path: bool 642 | :param skip_lineno: An optional performance flag. By default, when the 643 | script replaces something, it will tell you which line it is 644 | replacing on. This can be useful for tracking the places that 645 | changes occurred. When you turn this flag on however, it will not 646 | show the line numbers. This can give great performance increases 647 | because redbaron has trouble calculating the line number sometimes. 648 | :type skip_lineno: bool 649 | :param tometh_flag: tometh_flag is an optional feature flag. Once turned 650 | on, it will attempt to replace any QString/QVariant/etc apiv1.0 methods 651 | that are being used in your script. It is currently not smart enough to 652 | confirm that you don't have any custom objects with the same method 653 | signature to PyQt4's apiv1.0 ones. 654 | :type tometh_flag: bool 655 | """ 656 | 657 | def _is_dir(path): 658 | return True if os.path.isdir(os.path.join(folder, path)) else False 659 | 660 | # TODO: Might need to parse the text to remove whitespace at the EOL. 661 | # #101 at https://github.com/PyCQA/baron documents this issue. 662 | 663 | for fn in filter(is_py, [os.path.join(folder, fp) for fp in os.listdir(folder)]): 664 | process_file( 665 | fn, 666 | write_mode=write_mode, 667 | path=path, 668 | backup=backup, 669 | skip_lineno=skip_lineno, 670 | tometh_flag=tometh_flag, 671 | explicit_signals_flag=explicit_signals_flag 672 | ) 673 | MAIN_LOG.debug(color_text(text="-" * 50, color=ANSI.colors.black)) 674 | 675 | if not recursive: 676 | return 677 | 678 | for fn in filter(_is_dir, os.listdir(folder)): 679 | process_folder( 680 | os.path.join(folder, fn), 681 | recursive=recursive, 682 | write_mode=write_mode, 683 | path=path, 684 | backup=backup, 685 | skip_lineno=skip_lineno, 686 | tometh_flag=tometh_flag, 687 | explicit_signals_flag=explicit_signals_flag 688 | ) 689 | 690 | 691 | if __name__ == "__main__": 692 | # process_file("/dd/shows/DEVTD/user/work.ahughes/svn/assetbrowser/trunk/src/python/assetbrowser/workflow/widgets/custom.py", write=True) 693 | # process_file("/dd/shows/DEVTD/user/work.ahughes/svn/assetbrowser/trunk/src/python/assetbrowser/widget/Columns.py", write=True) 694 | # process_folder("/dd/shows/DEVTD/user/work.ahughes/svn/packages/ddg/trunk/src/python", recursive=True, write=True) 695 | # process_folder("/dd/shows/DEVTD/user/work.ahughes/svn/packages/ddqt/trunk/src/python", recursive=True, write=True) 696 | # process_file("/dd/shows/DEVTD/user/work.ahughes/svn/packages/nukepipeline/branches/nukepipeline_5/src/nuke/nodes/nukepipeline/ShotLook/shot_look.py", write=True) 697 | # process_folder("/dd/shows/DEVTD/user/work.ahughes/svn/packages/lightpipeline/trunk/src/python/lightpipeline/ui", recursive=True, write=True, fast_exit=True) 698 | # process_file("/dd/shows/DEVTD/user/work.ahughes/svn/packages/lightpipeline/trunk/src/python/lightpipeline/ui/errorDialogUI.py", write=True, fast_exit=True) 699 | # process_file("/dd/shows/DEVTD/user/work.ahughes/svn/packages/lightpipeline/trunk/src/python/lightpipeline/ui/HDRWidgetComponents.py", write=True, fast_exit=True) 700 | # process_folder("/dd/shows/DEVTD/user/work.ahughes/svn/packages/nukepipeline/branches/nukepipeline_5/src/", recursive=True, write=True, fast_exit=True) 701 | # process_folder("/dd/shows/DEVTD/user/work.ahughes/svn/packages/ddqt/trunk/src/python/ddqt", recursive=True, write=False, skip_lineno=True, tometh_flag=True) 702 | # folder = os.path.abspath("../../../../tests/sources") 703 | # process_folder(folder, recursive=True, write=True) 704 | process_folder("/dd/shows/DEVTD/user/work.ahughes/svn/packages/jstemplate_explorer/branches/qt_compat/src/bin", recursive=True, write_mode=WriteFlag.WRITE_TO_FILE, skip_lineno=True, tometh_flag=True) 705 | # process_file("/dd/shows/DEVTD/user/work.ahughes/svn/packages/refchef/branches/qt_compat/src/python/refchef/tagspanel.py", write_mode=WriteFlag.WRITE_TO_FILE, tometh_flag=True, explicit_signals_flag=False) 706 | # process_folder("/dd/shows/DEVTD/user/work.ahughes/svn/packages/refchef/branches/qt_compat/src/", write_mode=WriteFlag.WRITE_TO_FILE, tometh_flag=True, recursive=True) 707 | # process_file("/dd/shows/DEVTD/user/work.ahughes/svn/packages/ddqt/trunk/src/python/ddqt/gui/SnapshotModel.py", write=False, tometh_flag=True) 708 | -------------------------------------------------------------------------------- /tests/test_core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitaldomain/QtPyConvert/2a2d2b9121004c27598f9e1031cf6ebf41c0895a/tests/test_core/__init__.py -------------------------------------------------------------------------------- /tests/test_core/test_binding_supported.py: -------------------------------------------------------------------------------- 1 | from qt_py_convert.general import supported_binding 2 | 3 | 4 | def test_import_level_styles(): 5 | assert "PySide2" == supported_binding("PySide2") 6 | assert "PySide2" == supported_binding("PySide2.QtCore") 7 | assert "PySide2" == supported_binding("PySide2.QtWidgets") 8 | assert "PySide2" == supported_binding("PySide2.QtGui") 9 | 10 | assert "PySide" == supported_binding("PySide") 11 | assert "PySide" == supported_binding("PySide.QtCore") 12 | assert "PySide" == supported_binding("PySide.QtWidgets") 13 | assert "PySide" == supported_binding("PySide.QtGui") 14 | 15 | assert "PyQt5" == supported_binding("PyQt5") 16 | assert "PyQt5" == supported_binding("PyQt5.QtCore") 17 | assert "PyQt5" == supported_binding("PyQt5.QtWidgets") 18 | assert "PyQt5" == supported_binding("PyQt5.QtGui") 19 | 20 | assert "PyQt4" == supported_binding("PyQt4") 21 | assert "PyQt4" == supported_binding("PyQt4.QtCore") 22 | assert "PyQt4" == supported_binding("PyQt4.QtWidgets") 23 | assert "PyQt4" == supported_binding("PyQt4.QtGui") 24 | 25 | 26 | if __name__ == "__main__": 27 | import traceback 28 | _tests = filter( 29 | lambda key: True if key.startswith("test_") else False, 30 | globals().keys() 31 | ) 32 | 33 | failed = [] 34 | for test in _tests: 35 | try: 36 | print("Running %s" % test) 37 | globals()[test]() 38 | print(" %s succeeded!" % test) 39 | except AssertionError as err: 40 | print(" %s failed!" % test) 41 | failed.append((test, traceback.format_exc())) 42 | print("") 43 | for failure_name, failure_error in failed: 44 | print(""" 45 | ------------ %s FAILED ------------ 46 | %s 47 | """ % (failure_name, failure_error)) 48 | 49 | print( 50 | "\n\n%d failures, %d success, %s%%" % ( 51 | len(failed), 52 | len(_tests)-len(failed), 53 | "%.1f" % ((float(len(_tests)-len(failed))/len(_tests))*100) 54 | ) 55 | ) 56 | -------------------------------------------------------------------------------- /tests/test_core/test_replacements.py: -------------------------------------------------------------------------------- 1 | from qt_py_convert.run import run 2 | from qt_py_convert.diff import highlight_diffs 3 | 4 | 5 | def check(source, dest): 6 | aliases, mappings, dumps = run(source, True, True) 7 | try: 8 | assert dumps == dest 9 | except AssertionError as err: 10 | raise AssertionError("\n\"%s\"\n!=\n\"%s\"\n" % 11 | highlight_diffs(dumps, dest) 12 | ) 13 | 14 | 15 | def test_multiple_replacements_one_line(): 16 | check( 17 | """from PyQt4 import QtGui 18 | 19 | def fake_function(): 20 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) 21 | """, 22 | """from Qt import QtWidgets 23 | 24 | def fake_function(): 25 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) 26 | """) 27 | 28 | 29 | def test_qlineedit_replacement(): 30 | # This makes sure we don't have a regression for module mapping replacements. 31 | # For example QtCore.QLine matching QtWidgets.QLineEdit 32 | check( 33 | """from PyQt4.QtGui import QLineEdit 34 | 35 | l = QLineEdit() 36 | x = 42""", 37 | """from Qt import QtWidgets 38 | 39 | l = QtWidgets.QLineEdit() 40 | x = 42""" 41 | ) 42 | 43 | 44 | def test_refchef_bug_replacement(): 45 | check( 46 | """from PyQt4.QtCore import * 47 | 48 | QCoreApplication.setOrganizationName("Digital Domain") 49 | self.connect(self.thumbs_panel, SIGNAL('loadImage'), self.preview_panel.loadImage) 50 | self.connect(self.dir_panel, SIGNAL("dirClicked(QString, bool)"), self.thumbs_panel.loadFromDir) 51 | """, 52 | """from Qt import QtCore 53 | 54 | QtCore.QCoreApplication.setOrganizationName("Digital Domain") 55 | self.thumbs_panel.loadImage.connect(self.preview_panel.loadImage) 56 | self.dir_panel.dirClicked.connect(self.thumbs_panel.loadFromDir) 57 | """ 58 | ) 59 | 60 | 61 | if __name__ == "__main__": 62 | import traceback 63 | _tests = filter( 64 | lambda key: True if key.startswith("test_") else False, 65 | globals().keys() 66 | ) 67 | 68 | failed = [] 69 | for test in _tests: 70 | try: 71 | print("Running %s" % test) 72 | globals()[test]() 73 | print(" %s succeeded!" % test) 74 | except AssertionError as err: 75 | print(" %s failed!" % test) 76 | failed.append((test, traceback.format_exc())) 77 | print("") 78 | for failure_name, failure_error in failed: 79 | print(""" 80 | ------------ %s FAILED ------------ 81 | %s 82 | """ % (failure_name, failure_error)) 83 | 84 | print( 85 | "\n\n%d failures, %d success, %s%%" % ( 86 | len(failed), 87 | len(_tests)-len(failed), 88 | "%.1f" % ((float(len(_tests)-len(failed))/len(_tests))*100) 89 | ) 90 | ) 91 | -------------------------------------------------------------------------------- /tests/test_psep0101/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitaldomain/QtPyConvert/2a2d2b9121004c27598f9e1031cf6ebf41c0895a/tests/test_psep0101/__init__.py -------------------------------------------------------------------------------- /tests/test_psep0101/test_qsignal.py: -------------------------------------------------------------------------------- 1 | from qt_py_convert._modules.psep0101 import _qsignal 2 | 3 | 4 | def check_connection(source, dest, explicit=False): 5 | convert = _qsignal.process_connect(source, explicit=explicit) 6 | try: 7 | assert convert == dest 8 | except AssertionError as err: 9 | raise AssertionError("%s is not %s" % (convert, dest)) 10 | 11 | 12 | def check_emit(source, dest, explicit=False): 13 | convert = _qsignal.process_emit(source, explicit=explicit) 14 | try: 15 | assert convert == dest 16 | except AssertionError as err: 17 | raise AssertionError("%s is not %s" % (convert, dest)) 18 | 19 | 20 | def check_disconnect(source, dest, explicit=False): 21 | convert = _qsignal.process_disconnect(source, explicit=explicit) 22 | try: 23 | assert convert == dest 24 | except AssertionError as err: 25 | raise AssertionError("%s is not %s" % (convert, dest)) 26 | 27 | 28 | def test_connection_ddqt_1(): 29 | check_connection( 30 | '''QtCore.QObject.connect(self, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT("handle_click()"))''', 31 | '''self.clicked.connect(self.handle_click)''' 32 | ) 33 | 34 | 35 | def test_connection_texturepipeline_whitespace(): 36 | check_connection( 37 | '''self.seq_combo.connect (QtCore.SIGNAL("currentIndexChanged(int)"), 38 | self._updateShot)''', 39 | 'self.seq_combo.currentIndexChanged.connect(self._updateShot)' 40 | ) 41 | 42 | def test_connection_no_args(): 43 | check_connection( 44 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged()"), self.slot_filterBoxEdited)', 45 | "self.filterBox.textChanged.connect(self.slot_filterBoxEdited)" 46 | ) 47 | 48 | def test_connection_single_arg(): 49 | check_connection( 50 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(QString)"), self.slot_filterBoxEdited)', 51 | "self.filterBox.textChanged.connect(self.slot_filterBoxEdited)" 52 | ) 53 | 54 | def test_connection_single_arg_const(): 55 | check_connection( 56 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(const QString)"), self.slot_filterBoxEdited)', 57 | "self.filterBox.textChanged.connect(self.slot_filterBoxEdited)" 58 | ) 59 | 60 | def test_connection_single_arg_ref(): 61 | check_connection( 62 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(QString &)"), self.slot_filterBoxEdited)', 63 | "self.filterBox.textChanged.connect(self.slot_filterBoxEdited)" 64 | ) 65 | 66 | def test_connection_single_arg_ref_alt(): 67 | check_connection( 68 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(QString&)"), self.slot_filterBoxEdited)', 69 | "self.filterBox.textChanged.connect(self.slot_filterBoxEdited)" 70 | ) 71 | 72 | def test_connection_single_arg_constref(): 73 | check_connection( 74 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(const QString &)"), self.slot_filterBoxEdited)', 75 | "self.filterBox.textChanged.connect(self.slot_filterBoxEdited)" 76 | ) 77 | 78 | def test_connection_multi_arg(): 79 | check_connection( 80 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(const QString &, QVariant &)"), self.slot_filterBoxEdited)', 81 | "self.filterBox.textChanged.connect(self.slot_filterBoxEdited)" 82 | ) 83 | 84 | def test_connection_multi_arg_alt(): 85 | check_connection( 86 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(const QStringList &, QVariant &)"), self.slot_filterBoxEdited)', 87 | "self.filterBox.textChanged.connect(self.slot_filterBoxEdited)" 88 | ) 89 | 90 | 91 | def test_connection_qobject1(): 92 | check_connection( 93 | "QtCore.QObject.connect(self.ui.pushButton, QtCore.SIGNAL('clicked()'), self.refresher)", 94 | "self.ui.pushButton.clicked.connect(self.refresher)" 95 | ) 96 | 97 | 98 | def test_connection_qobject2(): 99 | check_connection( 100 | "QtCore.QObject.connect(self.ui.pushButton_selAll, QtCore.SIGNAL('clicked()'), self.selectAllChannels)", 101 | "self.ui.pushButton_selAll.clicked.connect(self.selectAllChannels)" 102 | ) 103 | 104 | 105 | def test_connection_qobject3(): 106 | check_connection( 107 | "QtCore.QObject.connect(self.ui.buttonBox, QtCore.SIGNAL('accepted()'), self.configureShuffles)", 108 | "self.ui.buttonBox.accepted.connect(self.configureShuffles)" 109 | ) 110 | 111 | 112 | def test_connection_qobject4(): 113 | check_connection( 114 | "QtCore.QObject.connect(self.ui.buttonBox, QtCore.SIGNAL('rejected()'), self.close)", 115 | "self.ui.buttonBox.rejected.connect(self.close)" 116 | ) 117 | 118 | 119 | def test_connection_qobject5(): 120 | check_connection( 121 | "QtCore.QObject.connect(self.ui.treeView.selectionModel(), QtCore.SIGNAL('selectionChanged(QItemSelection, QItemSelection)'), self.setViewerLayer)", 122 | "self.ui.treeView.selectionModel().selectionChanged.connect(self.setViewerLayer)" 123 | ) 124 | 125 | 126 | def test_connection_qobject6(): 127 | check_connection( 128 | "QtCore.QObject.connect(self.ui.treeView.selectionModel(), QtCore.SIGNAL('selectedIndexes()'), self.toggleViewerLayer)", 129 | "self.ui.treeView.selectionModel().selectedIndexes.connect(self.toggleViewerLayer)" 130 | ) 131 | 132 | 133 | def test_connection_qobject7(): 134 | check_connection( 135 | "QtCore.QObject.connect(self.ui.checkBox_inViewer, QtCore.SIGNAL('stateChanged(int)'), self.setViewerLayer)", 136 | "self.ui.checkBox_inViewer.stateChanged.connect(self.setViewerLayer)" 137 | ) 138 | 139 | 140 | def test_connection_qobject8(): 141 | check_connection( 142 | "QtCore.QObject.connect(self.ui.checkBox_addWrites, QtCore.SIGNAL('stateChanged(int)'), self.togglePath)", 143 | "self.ui.checkBox_addWrites.stateChanged.connect(self.togglePath)" 144 | ) 145 | 146 | def test_emit_no_args(): 147 | check_emit( 148 | 'self.emit(QtCore.SIGNAL("atomicPreflightChangedFrameRange()"))', 149 | "self.atomicPreflightChangedFrameRange.emit()" 150 | ) 151 | 152 | def test_emit_multi_args(): 153 | check_emit( 154 | 'self.dagEditor.emit(QtCore.SIGNAL("nodeNameEdited(PyQt_PyObject, PyQt_PyObject)"), node, newName)', 155 | "self.dagEditor.nodeNameEdited.emit(node, newName)" 156 | ) 157 | 158 | def test_disconnect_no_args(): 159 | check_disconnect( 160 | 'self.disconnect(self, QtCore.SIGNAL("textChanged()"), self.slot_textChanged)', 161 | "self.textChanged.disconnect(self.slot_textChanged)" 162 | ) 163 | 164 | def test_disconnect_single_arg_newlines(): 165 | check_disconnect( 166 | 'self.disconnect(self.batchGlobalsNode,\n QtCore.SIGNAL("attributeChanged(PyQt_PyObject)"),\n self.slot_raceGlobalsAttributeChanged)', 167 | "self.batchGlobalsNode.attributeChanged.disconnect(self.slot_raceGlobalsAttributeChanged)" 168 | ) 169 | 170 | def test_disconnect_local_var_newlines(): 171 | check_disconnect( 172 | 'self.disconnect(validation_message, QtCore.SIGNAL("atomicPreflightChangedFrameRange()"),\n self.slot_clientFrameRangeChanged)', 173 | "validation_message.atomicPreflightChangedFrameRange.disconnect(self.slot_clientFrameRangeChanged)" 174 | ) 175 | 176 | def test_disconnect_no_arg_alt(): 177 | check_disconnect( 178 | 'self.disconnect(self, QtCore.SIGNAL("atomicLevelEnvironmentChanged()"), self._slotLevelEnvironmentChanged)', 179 | "self.atomicLevelEnvironmentChanged.disconnect(self._slotLevelEnvironmentChanged)" 180 | ) 181 | 182 | def test_disconnect_single_arg(): 183 | check_disconnect( 184 | 'self.disconnect(self.sceneDag, QtCore.SIGNAL("dagChanged(PyQt_PyObject)"), self._slotDagChanged)', 185 | "self.sceneDag.dagChanged.disconnect(self._slotDagChanged)" 186 | ) 187 | 188 | 189 | def test_connect_old_style_slot(): 190 | check_connection( 191 | 'self.connect(cancel_button, QtCore.SIGNAL("clicked()"), QtCore.SLOT("close()"))', 192 | "cancel_button.clicked.connect(self.close)" 193 | ) 194 | 195 | 196 | def test_connect_converted_fromUTF(): 197 | check_connection( 198 | 'QtCore.QObject.connect(self.BUTTON_svn_check_out, QtCore.SIGNAL(_fromUtf8("pressed()")), OTLpublisher.checkOut)', 199 | "self.BUTTON_svn_check_out.pressed.connect(OTLpublisher.checkOut)" 200 | ) 201 | 202 | 203 | def test_connect_converted_fromUTF_1(): 204 | check_connection( 205 | 'QtCore.QObject.connect(self.BUTTON_svn_check_in, QtCore.SIGNAL(_fromUtf8("pressed()")), OTLpublisher.checkIn)', 206 | "self.BUTTON_svn_check_in.pressed.connect(OTLpublisher.checkIn)" 207 | ) 208 | 209 | 210 | def test_connect_converted_fromUTF_2(): 211 | check_connection( 212 | 'QtCore.QObject.connect(self.BUTTON_refresh_info, QtCore.SIGNAL(_fromUtf8("pressed()")), OTLpublisher.refreshInfo)', 213 | "self.BUTTON_refresh_info.pressed.connect(OTLpublisher.refreshInfo)" 214 | ) 215 | 216 | 217 | def test_connect_converted_fromUTF_3(): 218 | check_connection( 219 | 'QtCore.QObject.connect(self.BUTTON_svn_cancel_check_out, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.checkOutCancel)', 220 | "self.BUTTON_svn_cancel_check_out.released.connect(OTLpublisher.checkOutCancel)" 221 | ) 222 | 223 | 224 | def test_connect_converted_fromUTF_4(): 225 | check_connection( 226 | 'QtCore.QObject.connect(self.BUTTON_svn_revert_to_earlier_version, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.revertToEarlierSvnVersion)', 227 | "self.BUTTON_svn_revert_to_earlier_version.released.connect(OTLpublisher.revertToEarlierSvnVersion)" 228 | ) 229 | 230 | 231 | def test_connect_converted_fromUTF_5(): 232 | check_connection( 233 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_create, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.createDigitalAsset)', 234 | "self.BUTTON_digital_asset_create.released.connect(OTLpublisher.createDigitalAsset)" 235 | ) 236 | 237 | 238 | def test_connect_converted_fromUTF_6(): 239 | check_connection( 240 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_edit_type_properties, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.editTypeProperties)', 241 | "self.BUTTON_digital_asset_edit_type_properties.released.connect(OTLpublisher.editTypeProperties)" 242 | ) 243 | def test_connect_converted_fromUTF_7(): 244 | check_connection( 245 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_match_cur_def, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.matchCurDef)', 246 | "self.BUTTON_digital_asset_match_cur_def.released.connect(OTLpublisher.matchCurDef)" 247 | ) 248 | 249 | 250 | def test_connect_converted_fromUTF_8(): 251 | check_connection( 252 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_allow_editing, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.allowEditing)', 253 | "self.BUTTON_digital_asset_allow_editing.released.connect(OTLpublisher.allowEditing)" 254 | ) 255 | 256 | 257 | def test_connect_converted_fromUTF_9(): 258 | check_connection( 259 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_use_this_def, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.useThisDef)', 260 | "self.BUTTON_digital_asset_use_this_def.released.connect(OTLpublisher.useThisDef)" 261 | ) 262 | 263 | 264 | def test_connect_converted_fromUTF_10(): 265 | check_connection( 266 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_copy, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.copyDigitalAsset)', 267 | "self.BUTTON_digital_asset_copy.released.connect(OTLpublisher.copyDigitalAsset)" 268 | ) 269 | 270 | 271 | def test_connect_converted_fromUTF_11(): 272 | check_connection( 273 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_save, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.saveDigitalAsset)', 274 | "self.BUTTON_digital_asset_save.released.connect(OTLpublisher.saveDigitalAsset)" 275 | ) 276 | 277 | 278 | def test_connect_converted_fromUTF_12(): 279 | check_connection( 280 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_delete, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.deleteDigitalAsset)', 281 | "self.BUTTON_digital_asset_delete.released.connect(OTLpublisher.deleteDigitalAsset)" 282 | ) 283 | 284 | 285 | def test_connect_converted_fromUTF_13(): 286 | check_connection( 287 | 'QtCore.QObject.connect(self.BUTTON_select_node, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.selectNode)', 288 | "self.BUTTON_select_node.released.connect(OTLpublisher.selectNode)" 289 | ) 290 | 291 | 292 | def test_connect_converted_fromUTF_14(): 293 | check_connection( 294 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_publish, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.publishDigitalAsset)', 295 | "self.BUTTON_digital_asset_publish.released.connect(OTLpublisher.publishDigitalAsset)" 296 | ) 297 | 298 | 299 | def test_connect_converted_fromUTF_15(): 300 | check_connection( 301 | 'QtCore.QObject.connect(self.BUTTON_libsvn_copy, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.libSvnCopyDigitalAsset)', 302 | "self.BUTTON_libsvn_copy.released.connect(OTLpublisher.libSvnCopyDigitalAsset)" 303 | ) 304 | 305 | 306 | def test_connect_converted_fromUTF_16(): 307 | check_connection( 308 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_version_up, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.versionUp)', 309 | "self.BUTTON_digital_asset_version_up.released.connect(OTLpublisher.versionUp)" 310 | ) 311 | 312 | 313 | def test_connect_converted_fromUTF_17(): 314 | check_connection( 315 | 'QtCore.QObject.connect(self.BUTTON_digital_asset_op_type_manager, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.opTypeManager)', 316 | "self.BUTTON_digital_asset_op_type_manager.released.connect(OTLpublisher.opTypeManager)" 317 | ) 318 | 319 | 320 | def test_connect_converted_fromUTF_18(): 321 | check_connection( 322 | 'QtCore.QObject.connect(self.BUTTON_help, QtCore.SIGNAL(_fromUtf8("released()")), OTLpublisher.help)', 323 | "self.BUTTON_help.released.connect(OTLpublisher.help)" 324 | ) 325 | 326 | 327 | def test_connect_old_style_multiline(): 328 | check_connection( 329 | """self.connect( 330 | self.data.ui, 331 | QtCore.SIGNAL("launchPublishPopup"), 332 | self.publishDigitalAssetPopup 333 | )""", 334 | "self.data.ui.launchPublishPopup.connect(self.publishDigitalAssetPopup)" 335 | ) 336 | 337 | 338 | def test_connect_old_style_pyargs(): 339 | check_connection( 340 | 'QtCore.QObject.connect(self.ticket_tool, QtCore.SIGNAL(_fromUtf8("currentIndexChanged(unicode)")), Flaregun.infoEntered)', 341 | "self.ticket_tool.currentIndexChanged.connect(Flaregun.infoEntered)", 342 | ) 343 | 344 | 345 | def test_connect_old_style_no_owner(): 346 | check_connection( 347 | """self.__shot_combo.connect(QtCore.SIGNAL("currentIndexChanged(int)"), 348 | self.__updateLinkVersionLabel)""", 349 | """self.__shot_combo.currentIndexChanged.connect(self.__updateLinkVersionLabel)""", 350 | ) 351 | check_connection( 352 | """action.connect(QtCore.SIGNAL("triggered()"), self.selectPatches)""", 353 | """action.triggered.connect(self.selectPatches)""", 354 | ) 355 | check_connection( 356 | """channel_all_check.connect(QtCore.SIGNAL("stateChanged(int)"), 357 | self.__updateChannelStates)""", 358 | """channel_all_check.stateChanged.connect(self.__updateChannelStates)""", 359 | ) 360 | 361 | 362 | def test_connect_for_refchef(): 363 | check_connection( 364 | """self.connect(self.thumbs_panel, QtCore.SIGNAL('loadImage'), self.preview_panel.loadImage)""", 365 | """self.thumbs_panel.loadImage.connect(self.preview_panel.loadImage)""" 366 | ) 367 | 368 | 369 | def test_connect_for_refchef2(): 370 | check_connection( 371 | """self.connect(self.dir_panel, QtCore.SIGNAL("dirClicked(unicode, bool)"), self.thumbs_panel.loadFromDir)""", 372 | """self.dir_panel.dirClicked.connect(self.thumbs_panel.loadFromDir)""" 373 | ) 374 | 375 | 376 | def test_emit_for_refchef(): 377 | check_emit( 378 | """self.emit(SIGNAL('imageLoadInterrupted'))""", 379 | """self.imageLoadInterrupted.emit()""" 380 | ) 381 | 382 | 383 | def test_emit_for_refchef2(): 384 | check_emit( 385 | """self.emit(SIGNAL('imageLoaded'), self.image, self.is_linear, self.is_float)""", 386 | """self.imageLoaded.emit(self.image, self.is_linear, self.is_float)""" 387 | ) 388 | 389 | 390 | def test_emit_for_refchef3(): 391 | check_emit( 392 | """self.emit(SIGNAL('loadFromData'), data)""", 393 | """self.loadFromData.emit(data)""" 394 | ) 395 | 396 | 397 | def test_emit_for_refchef4(): 398 | check_emit( 399 | """self.emit(SIGNAL('itemClicked'), self.model().at(index.row()))""", 400 | """self.itemClicked.emit(self.model().at(index.row()))""" 401 | ) 402 | 403 | 404 | def test_emit_for_refchef5(): 405 | check_emit( 406 | """QObject.emit(self, SIGNAL("dataChanged(const QModelIndex&, const QModelIndex &)"), index, index)""", 407 | """self.dataChanged.emit(index, index)""" 408 | ) 409 | 410 | 411 | # --------------------------------------------------------------------------- # 412 | # Explicit signal testing 413 | # --------------------------------------------------------------------------- # 414 | def test_emit_for_refchef5_EXPLICIT(): 415 | check_emit( 416 | """QObject.emit(self, SIGNAL("dataChanged(const QModelIndex&, const QModelIndex &)"), index, index)""", 417 | """self.dataChanged[QModelIndex, QModelIndex].emit(index, index)""", 418 | explicit=True 419 | ) 420 | 421 | 422 | def test_connect_for_refchef2_EXPLICIT(): 423 | check_connection( 424 | """self.connect(self.dir_panel, QtCore.SIGNAL("dirClicked(unicode, bool)"), self.thumbs_panel.loadFromDir)""", 425 | """self.dir_panel.dirClicked[unicode, bool].connect(self.thumbs_panel.loadFromDir)""", 426 | explicit=True 427 | ) 428 | 429 | 430 | def test_connect_old_style_no_owner_EXPLICIT(): 431 | check_connection( 432 | """self.__shot_combo.connect(QtCore.SIGNAL("currentIndexChanged(int)"), 433 | self.__updateLinkVersionLabel)""", 434 | """self.__shot_combo.currentIndexChanged[int].connect(self.__updateLinkVersionLabel)""", 435 | explicit=True 436 | ) 437 | check_connection( 438 | """action.connect(QtCore.SIGNAL("triggered()"), self.selectPatches)""", 439 | """action.triggered.connect(self.selectPatches)""", 440 | explicit=True 441 | ) 442 | check_connection( 443 | """channel_all_check.connect(QtCore.SIGNAL("stateChanged(int)"), 444 | self.__updateChannelStates)""", 445 | """channel_all_check.stateChanged[int].connect(self.__updateChannelStates)""", 446 | explicit=True 447 | ) 448 | 449 | 450 | def test_connection_single_arg_ref_EXPLICIT(): 451 | check_connection( 452 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(QString &)"), self.slot_filterBoxEdited)', 453 | "self.filterBox.textChanged[str].connect(self.slot_filterBoxEdited)", 454 | explicit=True 455 | ) 456 | 457 | 458 | def test_connection_single_arg_const_EXPLICIT(): 459 | check_connection( 460 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(const QString)"), self.slot_filterBoxEdited)', 461 | "self.filterBox.textChanged[str].connect(self.slot_filterBoxEdited)", 462 | explicit=True 463 | ) 464 | 465 | 466 | def test_connection_single_arg_EXPLICIT(): 467 | check_connection( 468 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(QString)"), self.slot_filterBoxEdited)', 469 | "self.filterBox.textChanged[str].connect(self.slot_filterBoxEdited)", 470 | explicit=True 471 | ) 472 | 473 | 474 | def test_connection_texturepipeline_whitespace_EXPLICIT(): 475 | check_connection( 476 | '''self.seq_combo.connect (QtCore.SIGNAL("currentIndexChanged(int)"), 477 | self._updateShot)''', 478 | 'self.seq_combo.currentIndexChanged[int].connect(self._updateShot)', 479 | explicit=True 480 | ) 481 | 482 | 483 | def test_connection_multi_arg_EXPLICIT(): 484 | check_connection( 485 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(const QString &, QVariant &)"), self.slot_filterBoxEdited)', 486 | "self.filterBox.textChanged[str, object].connect(self.slot_filterBoxEdited)", 487 | explicit=True 488 | ) 489 | 490 | def test_connection_multi_arg_alt_EXPLICIT(): 491 | check_connection( 492 | 'self.connect(self.filterBox, QtCore.SIGNAL("textChanged(const QStringList &, QVariant &)"), self.slot_filterBoxEdited)', 493 | "self.filterBox.textChanged[list, object].connect(self.slot_filterBoxEdited)", 494 | explicit=True 495 | ) 496 | 497 | 498 | def test_connection_qobject5_EXPLICIT(): 499 | check_connection( 500 | "QtCore.QObject.connect(self.ui.treeView.selectionModel(), QtCore.SIGNAL('selectionChanged(QItemSelection, QItemSelection)'), self.setViewerLayer)", 501 | "self.ui.treeView.selectionModel().selectionChanged[QItemSelection, QItemSelection].connect(self.setViewerLayer)", 502 | explicit=True 503 | ) 504 | 505 | 506 | def test_connection_qobject7_EXPLICIT(): 507 | check_connection( 508 | "QtCore.QObject.connect(self.ui.checkBox_inViewer, QtCore.SIGNAL('stateChanged(int)'), self.setViewerLayer)", 509 | "self.ui.checkBox_inViewer.stateChanged[int].connect(self.setViewerLayer)", 510 | explicit=True 511 | ) 512 | 513 | 514 | def test_connection_qobject8_EXPLICIT(): 515 | check_connection( 516 | "QtCore.QObject.connect(self.ui.checkBox_addWrites, QtCore.SIGNAL('stateChanged(int)'), self.togglePath)", 517 | "self.ui.checkBox_addWrites.stateChanged[int].connect(self.togglePath)", 518 | explicit=True 519 | ) 520 | 521 | 522 | def test_connect_jstemplate_explorer_EXTRA_COMMA(): 523 | check_connection( 524 | """self.connect( 525 | self.__window.main_widget.template_widget.showBox, 526 | QtCore.SIGNAL("currentIndexChanged(int)"), 527 | self.__controller.loadTemplate, 528 | )""", 529 | """self.__window.main_widget.template_widget.showBox.currentIndexChanged.connect(self.__controller.loadTemplate)""" 530 | ) 531 | 532 | 533 | def test_connect_jstemplate_explorer_EXTRA_COMMA2(): 534 | check_connection( 535 | """self.connect( 536 | self.__window.main_widget.template_widget.templateTree, 537 | QtCore.SIGNAL("customContextMenuRequested(QPoint)"), 538 | self.__controller.doContextMenu, 539 | )""", 540 | """self.__window.main_widget.template_widget.templateTree.customContextMenuRequested.connect(self.__controller.doContextMenu)""" 541 | ) 542 | 543 | 544 | def test_connect_jstemplate_explorer_EXTRA_COMMA3(): 545 | check_connection( 546 | """self.connect( 547 | self.__window.main_widget.template_widget.fileSelection_PushButton, 548 | QtCore.SIGNAL("clicked()"), 549 | self.__controller.getFilePath, 550 | )""", 551 | """self.__window.main_widget.template_widget.fileSelection_PushButton.clicked.connect(self.__controller.getFilePath)""" 552 | ) 553 | 554 | 555 | def test_connect_jstemplate_explorer_EXTRA_COMMA4(): 556 | check_connection( 557 | """self.connect( 558 | self.__window.main_widget.refresh_Shortcut, 559 | QtCore.SIGNAL("activated()"), 560 | self.__controller.refresh, 561 | )""", 562 | """self.__window.main_widget.refresh_Shortcut.activated.connect(self.__controller.refresh)""" 563 | ) 564 | 565 | 566 | if __name__ == "__main__": 567 | import traceback 568 | _tests = filter( 569 | lambda key: True if key.startswith("test_") else False, 570 | globals().keys() 571 | ) 572 | 573 | failed = [] 574 | for test in _tests: 575 | try: 576 | print("Running %s" % test) 577 | globals()[test]() 578 | print(" %s succeeded!" % test) 579 | except AssertionError as err: 580 | print(" %s failed!" % test) 581 | failed.append((test, traceback.format_exc())) 582 | print("") 583 | for failure_name, failure_error in failed: 584 | print(""" 585 | ------------ %s FAILED ------------ 586 | %s 587 | """ % (failure_name, failure_error)) 588 | 589 | print( 590 | "\n\n%d failures, %d success, %s%%" % ( 591 | len(failed), 592 | len(_tests)-len(failed), 593 | "%.1f" % ((float(len(_tests)-len(failed))/len(_tests))*100) 594 | ) 595 | ) 596 | -------------------------------------------------------------------------------- /tests/test_psep0101/test_qvariant.py: -------------------------------------------------------------------------------- 1 | from qt_py_convert._modules.psep0101 import process 2 | from redbaron import redbaron 3 | from qt_py_convert.general import ALIAS_DICT 4 | 5 | 6 | def check(source, dest): 7 | red = redbaron.RedBaron(source) 8 | process(red, skip_lineno=True, tometh_flag=False) 9 | convert = red.dumps() 10 | try: 11 | assert convert == dest 12 | except AssertionError as err: 13 | raise AssertionError("\n%s\n!=\n%s" % (convert, dest)) 14 | 15 | 16 | def test_qvariant_basic(): 17 | check( 18 | 't = QVariant() # I should become None', 19 | 't = None # I should become None' 20 | ) 21 | 22 | 23 | def test_qvariant_list(): 24 | check( 25 | 'tt = QVariant("[23, 19]") # I should become list', 26 | 'tt = "[23, 19]" # I should become list' 27 | ) 28 | 29 | 30 | def test_qvariant_inside_other(): 31 | check( 32 | 'ttt = sum([QVariant("[23, 19]"), 42]) # I should become sum([42, 42])', 33 | 'ttt = sum(["[23, 19]", 42]) # I should become sum([42, 42])' 34 | ) 35 | 36 | 37 | def test_qvariant_potato(): 38 | check( 39 | 't = QVariant("foo()")', 40 | 't = "foo()"' 41 | ) 42 | 43 | 44 | def test_qvariant_error_basic(): 45 | s = """if isinstance( 46 | value, QVariant 47 | ): 48 | pass 49 | """ 50 | check( 51 | s, 52 | s, 53 | ) 54 | assert len(ALIAS_DICT["errors"]) == 1,\ 55 | "There are %d errors, there should be 1" % len(ALIAS_DICT["errors"]) 56 | 57 | 58 | if __name__ == "__main__": 59 | import traceback 60 | _tests = filter( 61 | lambda key: True if key.startswith("test_") else False, 62 | globals().keys() 63 | ) 64 | 65 | failed = [] 66 | for test in _tests: 67 | try: 68 | print("Running %s" % test) 69 | globals()[test]() 70 | print(" %s succeeded!" % test) 71 | except AssertionError as err: 72 | print(" %s failed!" % test) 73 | failed.append((test, traceback.format_exc())) 74 | print("") 75 | for failure_name, failure_error in failed: 76 | print(""" 77 | ------------ %s FAILED ------------ 78 | %s 79 | """ % (failure_name, failure_error)) 80 | 81 | print( 82 | "\n\n%d failures, %d success, %s%%" % ( 83 | len(failed), 84 | len(_tests)-len(failed), 85 | "%.1f" % ((float(len(_tests)-len(failed))/len(_tests))*100) 86 | ) 87 | ) 88 | -------------------------------------------------------------------------------- /tests/test_qtcompat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitaldomain/QtPyConvert/2a2d2b9121004c27598f9e1031cf6ebf41c0895a/tests/test_qtcompat/__init__.py -------------------------------------------------------------------------------- /tests/test_qtcompat/test_compatibility_members.py: -------------------------------------------------------------------------------- 1 | from qt_py_convert.run import run 2 | from qt_py_convert.general import highlight_diffs 3 | 4 | 5 | def check(source, dest): 6 | aliases, mappings, dumps = run(source, True, True) 7 | try: 8 | assert dumps == dest 9 | except AssertionError as err: 10 | raise AssertionError("\n\"%s\"\n!=\n\"%s\"\n" % 11 | highlight_diffs(dumps, dest) 12 | ) 13 | 14 | 15 | def test_wrapInstance_call_pyqt4(): 16 | check( 17 | """from PyQt4 import QtGui 18 | import sip 19 | self.maya_view_widget = sip.wrapinstance(long(self.maya_view.widget()), QtGui.QWidget)""", 20 | """from Qt import QtCompat, QtWidgets 21 | self.maya_view_widget = QtCompat.wrapInstance(long(self.maya_view.widget()), QtWidgets.QWidget) 22 | """ 23 | ) 24 | 25 | 26 | def test_wrapInstance_call_pyside(): 27 | check( 28 | """from PySide import QtGui 29 | import shiboken 30 | self.maya_view_widget = shiboken.wrapInstance(long(self.maya_view.widget()), QtGui.QWidget)""", 31 | """from Qt import QtCompat, QtWidgets 32 | self.maya_view_widget = QtCompat.wrapInstance(long(self.maya_view.widget()), QtWidgets.QWidget) 33 | """ 34 | ) 35 | 36 | 37 | def test_qapplication_translate_basic(): 38 | check( 39 | """from PySide import QtGui 40 | 41 | QtGui.QApplication.translate(context, text, disambig) 42 | """, 43 | """from Qt import QtCompat 44 | 45 | QtCompat.translate(context, text, disambig) 46 | """ 47 | ) 48 | 49 | 50 | def test_qcoreapplication_translate_basic(): 51 | check( 52 | """from PySide import QtCore 53 | 54 | QtCore.QCoreApplication.translate(context, text, disambig) 55 | """, 56 | """from Qt import QtCompat 57 | 58 | QtCompat.translate(context, text, disambig) 59 | """ 60 | ) 61 | 62 | 63 | def test_qinstallmessagehandler_basic(): 64 | check( 65 | """from PySide import QtCore 66 | 67 | def handler(*args): 68 | pass 69 | 70 | QtCore.qInstallMsgHandler(handler) 71 | """, 72 | """from Qt import QtCompat 73 | 74 | def handler(*args): 75 | pass 76 | 77 | QtCompat.qInstallMessageHandler(handler) 78 | """ 79 | ) 80 | 81 | 82 | if __name__ == "__main__": 83 | import traceback 84 | _tests = filter( 85 | lambda key: True if key.startswith("test_") else False, 86 | globals().keys() 87 | ) 88 | 89 | failed = [] 90 | for test in _tests: 91 | try: 92 | print("Running %s" % test) 93 | globals()[test]() 94 | print(" %s succeeded!" % test) 95 | except AssertionError as err: 96 | print(" %s failed!" % test) 97 | failed.append((test, traceback.format_exc())) 98 | print("") 99 | for failure_name, failure_error in failed: 100 | print(""" 101 | ------------ %s FAILED ------------ 102 | %s 103 | """ % (failure_name, failure_error)) 104 | 105 | print( 106 | "\n\n%d failures, %d success, %s%%" % ( 107 | len(failed), 108 | len(_tests)-len(failed), 109 | "%.1f" % ((float(len(_tests)-len(failed))/len(_tests))*100) 110 | ) 111 | ) 112 | --------------------------------------------------------------------------------