├── .coveragerc ├── .github └── workflows │ ├── publish.yml │ ├── pythonpackage.yml │ └── test.yml ├── .gitignore ├── .gitlab-ci.yml ├── .idea ├── modules.xml └── vcs.xml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── __init__.py ├── pyqt5ac.py ├── setup.py └── tests ├── __init__.py └── test_pyqt5ac.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | __init__.py 4 | setup.py 5 | tests/* -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.8 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install setuptools wheel twine 22 | pip install -e . 23 | - name: Build and publish 24 | env: 25 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 26 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 27 | run: | 28 | python setup.py sdist bdist_wheel 29 | twine upload dist/* 30 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.5, 3.6, 3.7, 3.8] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -e .[test] 30 | - name: Test with pytest 31 | run: | 32 | pytest . --cov=pyqt5ac --junitxml=report.xml --cov-report html:coverage --cov-report term -v --color=yes 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.8 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -e .[test] 20 | - name: Test with pytest 21 | run: | 22 | python -m pytest . --cov=pyqt5ac --junitxml=report.xml --cov-report html:coverage --cov-report term -v --color=yes 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # PyCharm Project specific 104 | .idea/workspace.xml 105 | .idea/tasks.xml 106 | .idea/misc.xml 107 | .idea/dictionaries 108 | .idea/inspectionProfiles 109 | 110 | # Ignore IML file for project since it contains path dependent information 111 | *.iml 112 | 113 | # mypy 114 | .mypy_cache/ 115 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: python:3.8 2 | 3 | before_script: 4 | - python -V 5 | 6 | stages: 7 | - test 8 | - QA 9 | 10 | .test script: &test_script 11 | - pip install -e .[test] 12 | - pytest . --cov=pyqt5ac --junitxml=report.xml --cov-report html:coverage --cov-report term -vx --color=yes 13 | 14 | .test template: 15 | stage: test 16 | script: 17 | - *test_script 18 | artifacts: 19 | reports: 20 | junit: report.xml 21 | 22 | Test latest: 23 | extends: .test template 24 | image: python:latest 25 | 26 | Test 3.8: 27 | extends: .test template 28 | image: python:3.8 29 | script: 30 | - *test_script 31 | - grep "Coverage for pyqt5ac.py" coverage/pyqt5ac_py.html 32 | artifacts: 33 | name: coverage-report 34 | paths: 35 | - coverage 36 | 37 | Test 3.6: 38 | extends: .test template 39 | image: python:3.6 40 | 41 | Test 3.5: 42 | extends: .test template 43 | image: python:3.5 44 | 45 | Flake: 46 | stage: QA 47 | needs: [] 48 | script: 49 | - pip install flake8 50 | - flake8 --max-line-length=120 pyqt5ac.py 51 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.2.1] - 2020-05-11 8 | ### Fixed 9 | - AttributeError when using commandline interface 10 | 11 | ## [1.2.0] - 2020-05-09 12 | ### Added 13 | - (#10) Ensure eventually generated folders contain `__init__.py` files (ZanSara) 14 | 15 | ## [1.1.0] - 2020-04-04 16 | ### Added 17 | - (#2) Custom variables support in configuration (ZanSara) 18 | 19 | ### Changed 20 | - (#12) Update deprecated YAML loading usage 21 | - (#21) Declare PyQt5 as required dependency 22 | 23 | ### Deprecated 24 | - (#14) Deprecate JSON configuration 25 | 26 | ## [1.0.4] - 2020-03-28 27 | ### Changed 28 | - Testing auto deployment via PyPi 29 | 30 | ## [1.0.3] - 2018-11-09 31 | ### Changed 32 | - (#1) Switch to using pyuic5 and pyrcc5 from within Python modules (addisonElliott) -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at zsolt@kovaridev.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Addison Elliott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/addisonElliott/pyqt5ac/workflows/CI/badge.svg)](https://github.com/addisonElliott/pyqt5ac/actions) 2 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyqt5ac.svg)](https://pypi.org/project/pyqt5ac/) 3 | [![PyPI](https://img.shields.io/pypi/v/pyqt5ac.svg)](https://pypi.org/project/pyqt5ac/) 4 | [![PyPI - License](https://img.shields.io/pypi/l/pyqt5ac.svg)](https://github.com/addisonElliott/pyqt5ac/blob/master/LICENSE) 5 | 6 | * [PyQt5 Auto Compiler (pyqt5ac)](#pyqt5-auto-compiler-pyqt5ac) 7 | * [Enter pyqt5ac!](#enter-pyqt5ac) 8 | * [Installing](#installing) 9 | * [Getting Started](#getting-started) 10 | * [Running from Command Line](#running-from-command-line) 11 | * [Running from Python Script](#running-from-python-script) 12 | * [Configuration Options](#configuration-options) 13 | * [Example](#example) 14 | * [Option 1: YAML Config File (Recommended)](#option-1-yaml-config-file-recommended) 15 | * [Option 2: JSON Config File (Deprecated)](#option-2-json-config-file-deprecated) 16 | * [Option 3: Python Script](#option-3-python-script) 17 | * [Option 4: Command Line](#option-4-command-line) 18 | * [Resulting File Structure](#resulting-file-structure) 19 | * [Support](#support) 20 | * [License](#license) 21 | 22 | PyQt5 Auto Compiler (pyqt5ac) 23 | ============================= 24 | 25 | pyqt5ac is a Python package for automatically compiling Qt's UI and QRC files into Python files. 26 | 27 | In PyQt5, [Qt Designer](https://www.qt.io/) is the application used to create a GUI using a drag-and-drop interface. This interface is stored in a *.ui* file and any resources such as images or icons are stored in a *.qrc* file. 28 | 29 | These two filetypes must be compiled into Python files before they can be used in your Python program. There are a few ways to go about this currently: 30 | 1. Manually compile the files using the command line and pyuic5 for *.ui* files and pyrcc5 for *.qrc* files. 31 | 2. Compile the files each time the application is started up by calling pyuic5 and pyrcc5 within your Python script 32 | 33 | The downside to the first method is that it can be a tedious endeavor to compile the files, especially when one is faced with a larger project with many of these files that need to be compiled. Although the second method eliminates the tediousness of compilation, these files are compiled **every** time you run your script, regardless of if anything has been changed. This can cause a hit in performance and take longer to startup your script. 34 | 35 | ### Enter **pyqt5ac**! 36 | 37 | pyqt5ac provides a command-line interface (CLI) that searches through your files and automatically compiles any *.ui* or *.qrc* files. In addition, pyqt5ac can be called from your Python script. In both instances, **ui and resource files are only compiled if they have been updated**. 38 | 39 | Installing 40 | ========== 41 | 42 | pyqt5ac is currently available on [PyPi](https://pypi.python.org/pypi/pyqt5ac/). The simplest way to 43 | install alone is using ``pip`` at a command line 44 | 45 | pip install pyqt5ac 46 | 47 | which installs the latest release. To install the latest code from the repository (usually stable, but may have 48 | undocumented changes or bugs) 49 | 50 | pip install git+https://github.com/addisonElliott/pyqt5ac.git 51 | 52 | For developers, you can clone the pyqt5ac repository and run the ``setup.py`` file. Use the following commands to get 53 | a copy from GitHub and install all dependencies 54 | 55 | git clone https://github.com/addisonElliott/pyqt5ac.git 56 | cd pyqt5ac 57 | pip install .[dev] 58 | 59 | to install in 'develop' or 'editable' mode, where changes can be made to the local working code and Python will use 60 | the updated code. 61 | 62 | Getting Started 63 | =============== 64 | 65 | Running from Command Line 66 | ------------------------- 67 | 68 | If pyqt5ac is installed via pip, the command line interface can be called like any Unix based program in the terminal 69 | 70 | pyqt5ac [OPTIONS] [IOPATHS]... 71 | 72 | In the interface, the options have slightly different names so reference the help file of the interface for more information. The largest difference is that the ioPaths argument is instead a list of space delineated paths where the even items are the source file expression and the odd items are the destination file expression. 73 | 74 | The help file of the interface can be run as 75 | 76 | pyqt5ac --help 77 | 78 | Running from Python Script 79 | -------------------------- 80 | 81 | The following snippet of code below demonstrates how to call pyqt5ac from your Python script 82 | 83 | ```python 84 | import pyqt5ac 85 | 86 | pyqt5ac.main(rccOptions='', uicOptions='--from-imports', force=False, initPackage=True, config='', 87 | ioPaths=[['gui/*.ui', 'generated/%%FILENAME%%_ui.py'], 88 | ['resources/*.qrc', 'generated/%%FILENAME%%_rc.py']]) 89 | ``` 90 | 91 | Configuration Options 92 | ===================== 93 | 94 | All of the options that can be specified to pyqt5ac can also be placed in a configuration file (JSON or YAML). My recommendation is to use a configuration file to allow easy compilation of your software. For testing purposes, I would use the options in the command line interface to make get everything working and then transcribe that into a configuration file for repeated use. 95 | 96 | Whether running via the command line or from a script, the arguments and options that can be given are the same. The valid options are: 97 | * **rccOptions** - Additional options to pass to the resource compiler. See the man page of pyrcc5 for more information on options. An example of a valid option would be "-compress 1". Default is to pass no options. 98 | * **uicOptions** - Additional options to pass to the UI compiler. See the man page of pyuic5 for more information on options. An example of a valid option would be '--from-imports'. Default is to pass no options. 99 | * **force** - Specifies whether to force compile all of the files found. The default is false meaning only outdated files will be compiled. 100 | * **config** - JSON or YAML configuration file that contains information about these parameters. 101 | * **ioPaths** - This is a 2D list containing information about what source files to compile and where to place the source files. The first column is the source file global expression (meaning you can use wildcards, ** for recursive folder search, ? for options, etc to match filenames) and the second column is the destination file expression. The destination file expression recognizes 'special' variables that will be replaced with information from the source filename: 102 | * %%FILENAME%% - Filename of the source file without the extension 103 | * %%EXT%% - Extension excluding the period of the file (e.g. ui or qrc) 104 | * %%DIRNAME%% - Directory of the source file 105 | * **variables** - custom variables that can be used in the definition of the paths in **ioPaths**. For example, to limit the search of files to a specific directory, one can define a variable `BASEDIR` and then use it as `%%BASEDIR%%/gui/*.ui*` 106 | * **init_package** - If specified, an empty `__init__.py` file is also generated in every output directory if missing. Does not overwrite existing `__init__.py`. Default value is `True`. 107 | 108 | Note that all relative paths are resolved from the configuration file location, if given through a config file, or from the current working directory otherwise. 109 | 110 | Example 111 | ======= 112 | 113 | Take the following file structure as an example project where any UI and QRC files need to be compiled. Assume that pyuic5 and pyrcc5 are located in /usr/bin and that '--from-imports' is desired for the UIC compiler. 114 | 115 | ``` 116 | |-- gui 117 | | |-- mainWindow.ui 118 | | |-- addDataDialog.ui 119 | | `-- saveDataDialog.ui 120 | |-- resources 121 | | |-- images 122 | | |-- stylesheets 123 | | |-- app.qrc 124 | | `-- style.qrc 125 | |-- modules 126 | | |-- welcome 127 | | | |-- module.ui 128 | | | `-- resources 129 | | | |-- images 130 | | | `-- module.qrc 131 | | `-- dataProbe 132 | | |-- module.ui 133 | | `-- resources 134 | | |-- images 135 | | `-- module.qrc 136 | ``` 137 | 138 | The sections below demonstrate how to setup pyqt5ac to compile the necessary files given the file structure above. 139 | 140 | Option 1: YAML Config File (Recommended) 141 | --------------------------------------- 142 | 143 | ```YAML 144 | ioPaths: 145 | - 146 | - "gui/*.ui" 147 | - "generated/%%FILENAME%%_ui.py" 148 | - 149 | - "resources/*.qrc" 150 | - "generated/%%FILENAME_%%%%EXT%%.py" 151 | - 152 | - "modules/*/*.ui" 153 | - "%%DIRNAME%%/generated/%%FILENAME%%_ui.py" 154 | - 155 | - "modules/*/resources/*.qrc" 156 | - "%%DIRNAME%%/generated/%%FILENAME%%_rc.py" 157 | 158 | uic_options: --from-imports 159 | init_package: True 160 | force: False 161 | ``` 162 | 163 | Now run pyqt5ac from the command line or Python script using your config file: 164 | ```bash 165 | pyqt5ac --config config.yml 166 | ``` 167 | 168 | or 169 | ```python 170 | import pyqt5ac 171 | 172 | pyqt5ac.main(config='config.yml') 173 | ``` 174 | 175 | Option 2: JSON Config File (Deprecated) 176 | --------------------------------------- 177 | 178 | ```JSON 179 | { 180 | "ioPaths": [ 181 | ["gui/*.ui", "generated/%%FILENAME%%_ui.py"], 182 | ["resources/*.qrc", "generated/%%FILENAME%%_rc.py"], 183 | ["modules/*/*.ui", "%%DIRNAME%%/generated/%%FILENAME%%_ui.py"], 184 | ["modules/*/resources/*.qrc", "%%DIRNAME%%/generated/%%FILENAME%%_rc.py"] 185 | ], 186 | "rcc_options": "", 187 | "uic_options": "--from-imports", 188 | "init_package": true, 189 | "force": false 190 | } 191 | ``` 192 | 193 | Now run pyqt5ac from the command line or Python script using your config file: 194 | ```bash 195 | pyqt5ac --config config.yml 196 | ``` 197 | 198 | or 199 | ```python 200 | import pyqt5ac 201 | 202 | pyqt5ac.main(config='config.yml') 203 | ``` 204 | 205 | Option 3: Python Script 206 | ----------------------- 207 | 208 | ```python 209 | import pyqt5ac 210 | 211 | pyqt5ac.main(uicOptions='--from-imports', force=False, initPackage=True, ioPaths=[ 212 | ['gui/*.ui', 'generated/%%FILENAME%%_ui.py'], 213 | ['resources/*.qrc', 'generated/%%FILENAME%%_rc.py'], 214 | ['modules/*/*.ui', '%%DIRNAME%%/generated/%%FILENAME%%_ui.py'], 215 | ['modules/*/resources/*.qrc', '%%DIRNAME%%/generated/%%FILENAME%%_rc.py'] 216 | ]) 217 | ``` 218 | 219 | Option 4: Command Line 220 | ---------------------- 221 | 222 | ```bash 223 | pyqt5ac --uic_options "--from-imports" gui/*.ui generated/%%FILENAME%%_ui.py resources/*.qrc generated/%%FILENAME%%_rc.py modules/*/*.ui %%DIRNAME%%/generated/%%FILENAME%%_ui.py modules/*/resources/*.qrc %%DIRNAME%%/generated/%%FILENAME%%_rc.py 224 | ``` 225 | 226 | Resulting File Structure 227 | ------------------------ 228 | 229 | ``` 230 | |-- gui 231 | | |-- mainWindow.ui 232 | | |-- addDataDialog.ui 233 | | `-- saveDataDialog.ui 234 | |-- resources 235 | | |-- images 236 | | |-- stylesheets 237 | | |-- app.qrc 238 | | `-- style.qrc 239 | |-- generated 240 | | |-- __init__.py_ 241 | | |-- mainWindow_ui.py 242 | | |-- addDataDialog_ui.py 243 | | |-- saveDataDialog_ui.py 244 | | |-- app_rc.py 245 | | `-- style_rc.py 246 | |-- modules 247 | | |-- welcome 248 | | | |-- module.ui 249 | | | |-- resources 250 | | | | |-- images 251 | | | | `-- module.qrc 252 | | | `-- generated 253 | | | |-- module_ui.py 254 | | | `-- module_rc.py 255 | | `-- dataProbe 256 | | |-- module.ui 257 | | |-- resources 258 | | | |-- images 259 | | | `-- module.qrc 260 | | `-- generated 261 | | |-- module_ui.py 262 | | `-- module_rc.py 263 | ``` 264 | 265 | Support 266 | ======= 267 | 268 | Issues and pull requests are encouraged! 269 | 270 | Bugs can be submitted through the [issue tracker](https://github.com/addisonElliott/pyqt5ac/issues). 271 | 272 | Pull requests are welcome too! 273 | 274 | License 275 | ================= 276 | 277 | pyqt5ac has an [MIT-based license](https://github.com/addisonElliott/pyqt5ac/blob/master/LICENSE). 278 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/pyqt5ac/5945a85f6230a97f3467c79441d87f5f9cdb478d/__init__.py -------------------------------------------------------------------------------- /pyqt5ac.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import json 3 | import os 4 | import shlex 5 | import subprocess 6 | import sys 7 | 8 | import click 9 | import yaml 10 | 11 | __version__ = '1.2.1' 12 | 13 | 14 | # Takes information about command and creates an argument list from it 15 | # In addition to an argument list, a 'cleaner' string is returned to be shown to the user 16 | # This essentially replaces 'python -m XXX' with the command parameter 17 | def _buildCommand(module, command, options, sourceFilename, destFilename): 18 | # Split options string into a list of options that is space-delineated 19 | # shlex.split is used rather than str.split to follow common shell rules such as strings in quotes are considered 20 | # one argument, even with spaces 21 | optionsList = shlex.split(options) 22 | 23 | # List of arguments with the first argument being the command to run 24 | # This is the argument list that will be actually ran by using sys.executable to get the current Python executable 25 | # running this program. 26 | argList = [sys.executable, '-m', module] + optionsList + ['-o', destFilename, sourceFilename] 27 | 28 | # However, for showing the user what command was ran, we will replace the 'python -m XXX' with pyuic5 or pyrcc5 to 29 | # make it look cleaner 30 | # Create one command string by escaping each argument and joining together with spaces 31 | cleanArgList = [command] + argList[3:] 32 | commandString = ' '.join([shlex.quote(arg) for arg in cleanArgList]) 33 | 34 | return argList, commandString 35 | 36 | 37 | def _isOutdated(src, dst, isQRCFile): 38 | outdated = (not os.path.exists(dst) or 39 | (os.path.getmtime(src) > os.path.getmtime(dst))) 40 | 41 | if not outdated and isQRCFile: 42 | # For qrc files, we need to check each individual resources. 43 | # If one of them is newer than the dst file, the qrc file must be considered as outdated. 44 | # File paths are relative to the qrc file path 45 | qrcParentDir = os.path.dirname(src) 46 | 47 | with open(src, 'r') as f: 48 | lines = f.readlines() 49 | lines = [line for line in lines if '' in line] 50 | 51 | cwd = os.getcwd() 52 | os.chdir(qrcParentDir) 53 | 54 | for line in lines: 55 | filename = line.replace('', '').replace('', '').strip() 56 | filename = os.path.abspath(filename) 57 | 58 | if os.path.getmtime(filename) > os.path.getmtime(dst): 59 | outdated = True 60 | break 61 | 62 | os.chdir(cwd) 63 | 64 | return outdated 65 | 66 | 67 | @click.command(name='pyqt5ac') 68 | @click.option('--rcc_options', 'rccOptions', default='', 69 | help='Additional options to pass to resource compiler [default: none]') 70 | @click.option('--uic_options', 'uicOptions', default='', 71 | help='Additional options to pass to UI compiler [default: none]') 72 | @click.option('--config', '-c', default='', type=click.Path(exists=True, file_okay=True, dir_okay=False), 73 | help='JSON or YAML file containing the configuration parameters') 74 | @click.option('--force', default=False, is_flag=True, help='Compile all files regardless of last modification time') 75 | @click.option('--init-package', 'initPackage', default=True, is_flag=True, 76 | help='Ensures that the folder containing the generated files is a Python subpackage ' 77 | '(i.e. it contains a file called __init__.py') 78 | @click.argument('iopaths', nargs=-1, required=False) 79 | @click.version_option(__version__) 80 | def cli(rccOptions, uicOptions, force, config, iopaths=(), initPackage=True): 81 | """Compile PyQt5 UI/QRC files into Python 82 | 83 | IOPATHS argument is a space delineated pair of glob expressions that specify the source files to compile as the 84 | first item in the pair and the path of the output compiled file for the second item. Multiple pairs of source and 85 | destination paths are allowed in IOPATHS. 86 | 87 | \b 88 | The destination path argument supports variables that are replaced based on the 89 | target source file: 90 | * %%FILENAME%% - Filename of the source file without the extension 91 | * %%EXT%% - Extension excluding the period of the file (e.g. ui or qrc) 92 | * %%DIRNAME%% - Directory of the source file 93 | 94 | Files that match a given source path expression are compiled if and only if the file has been modified since the 95 | last compilation unless the FORCE flag is set. If the destination file does not exist, then the file is compiled. 96 | 97 | A JSON or YAML configuration file path can be specified using the config option. See the GitHub page for example 98 | config files. 99 | 100 | \b 101 | Example: 102 | gui 103 | --->example.ui 104 | resources 105 | --->test.qrc 106 | 107 | \b 108 | Command: 109 | pyqt5ac gui/*.ui generated/%%FILENAME%%_ui.py resources/*.qrc generated/%%FILENAME%%_rc.py 110 | 111 | \b 112 | Results in: 113 | generated 114 | --->example_ui.py 115 | --->test_rc.py 116 | 117 | Author: Addison Elliott 118 | """ 119 | 120 | # iopaths is a 1D list containing pairs of the source and destination file expressions 121 | # So the list goes something like this: 122 | # [sourceFileExpr1, destFileExpr1, sourceFileExpr2, destFileExpr2, sourceFileExpr3, destFileExpr3] 123 | # 124 | # When calling the main function, it requires that ioPaths be a 2D list with 1st column source file expression and 125 | # second column the destination file expression. 126 | ioPaths = list(zip(iopaths[::2], iopaths[1::2])) 127 | 128 | main(rccOptions=rccOptions, uicOptions=uicOptions, force=force, config=config, ioPaths=ioPaths, 129 | initPackage=initPackage) 130 | 131 | 132 | def replaceVariables(variables_definition, string_with_variables): 133 | """ 134 | Performs variable replacements into the given string 135 | :param variables_definition: mapping variable_name - variable value. Matching names encased into %% will be replaces 136 | by their respective value found in the mapping (case-sensitive) 137 | :param string_with_variables: String where to replace the variable names (enclosed into %%'s) with their respective 138 | values found in the variables_definition 139 | :return: the input string with its variables replaced. 140 | """ 141 | for variable_name, variable_value in variables_definition.items(): 142 | string_with_variables = string_with_variables.replace("%%{}%%".format(variable_name), variable_value) 143 | return string_with_variables 144 | 145 | 146 | def resolvePath(path: str, reference_path: str) -> str: 147 | """ 148 | Translates relative paths into absolute paths, using the reference path as base. 149 | Meaningful reference values for the caller might be the configuration file path, the script's path or the current 150 | working directory. 151 | :param path: path to resolve. 152 | :param reference_path: path to be used as a reference to resolve absolute paths 153 | :return: an absolute path corresponding to the relative path passed in input if it was relative, or the unchanged 154 | input if it was an absolute path. 155 | :raises: ValueError if the reference path is not absolute 156 | """ 157 | if not os.path.isabs(path): 158 | if not os.path.isabs(reference_path): 159 | raise ValueError("The reference path must be absolute.") 160 | return os.path.join(reference_path, path) 161 | return path 162 | 163 | 164 | def main(rccOptions='', uicOptions='', force=False, config='', ioPaths=(), variables=None, initPackage=True): 165 | if config: 166 | with open(config, 'r') as fh: 167 | if config.endswith('.yml'): 168 | # Load YAML file 169 | configData = yaml.load(fh, Loader=yaml.FullLoader) 170 | else: 171 | click.secho('JSON usage is deprecated and will be removed in 2.0.0. Use YML configuration instead', 172 | fg='yellow') 173 | # Assume JSON file 174 | configData = json.load(fh) 175 | 176 | # configData variable is a dictionary where the keys are the names of the configuration 177 | # Load the keys and use the default value if nothing is specified 178 | rccOptions = configData.get('rcc_options', rccOptions) 179 | uicOptions = configData.get('uic_options', uicOptions) 180 | force = configData.get('force', force) 181 | ioPaths = configData.get('ioPaths', ioPaths) 182 | variables = configData.get('variables', variables) 183 | initPackage = configData.get('init_package', initPackage) 184 | 185 | # Validate the custom variables 186 | if variables is None: 187 | variables = {} 188 | if 'FILENAME' in variables.keys() or 'EXT' in variables.keys() or 'DIRNAME' in variables.keys(): 189 | raise ValueError("Custom variables cannot be called FILENAME, EXT or DIRNAME.") 190 | 191 | # Loop through the list of io paths 192 | for sourceFileExpr, destFileExpr in ioPaths: 193 | foundItem = False 194 | 195 | # Replace instances of the variables with the actual values of the available variables 196 | sourceFileExpr = replaceVariables(variables, sourceFileExpr) 197 | 198 | # Retrieve the absolute path to the source files 199 | sourceFileExpr = resolvePath(sourceFileExpr, (os.path.dirname(config) or os.getcwd())) 200 | 201 | # Find files that match the source filename expression given 202 | for sourceFilename in glob.glob(sourceFileExpr, recursive=True): 203 | # If the filename does not exist, not sure why this would ever occur, but show a warning 204 | if not os.path.exists(sourceFilename): 205 | click.secho('Skipping target %s, file not found' % sourceFilename, fg='yellow') 206 | continue 207 | 208 | foundItem = True 209 | 210 | # Split the source filename into directory and basename 211 | # Then split the basename into filename and extension 212 | # 213 | # Ex: C:/Users/addis/Documents/PythonProjects/PATS/gui/mainWindow.ui 214 | # dirname = C:/Users/addis/Documents/PythonProjects/PATS/gui 215 | # basename = mainWindow.ui 216 | # filename = mainWindow 217 | # ext = .ui 218 | dirname, basename = os.path.split(sourceFilename) 219 | filename, ext = os.path.splitext(basename) 220 | 221 | # Replace instances of the variables with the actual values from the source filename 222 | variables.update({'FILENAME': filename, 'EXT': ext[1:], 'DIRNAME': dirname}) 223 | destFilename = replaceVariables(variables, destFileExpr) 224 | 225 | # Retrieve the absolute path to the destination files 226 | destFilename = resolvePath(destFilename, (os.path.dirname(config) or os.getcwd())) 227 | 228 | if ext == '.ui': 229 | isQRCFile = False 230 | module = 'PyQt5.uic.pyuic' 231 | command = 'pyuic5' 232 | options = uicOptions 233 | elif ext == '.qrc': 234 | isQRCFile = True 235 | module = 'PyQt5.pyrcc_main' 236 | command = 'pyrcc5' 237 | options = rccOptions 238 | else: 239 | click.secho('Unknown target %s found' % sourceFilename, fg='yellow') 240 | continue 241 | 242 | # Create all directories to the destination filename and do nothing if they already exist 243 | dest_file_directory = os.path.dirname(destFilename) 244 | os.makedirs(dest_file_directory, exist_ok=True) 245 | 246 | # Ensure __init__.py is present and, if it's missing, generate it 247 | if initPackage: 248 | with open(os.path.join(dest_file_directory, "__init__.py"), 'a'): 249 | pass 250 | 251 | # If we are force compiling everything or the source file is outdated, then compile, otherwise skip! 252 | if force or _isOutdated(sourceFilename, destFilename, isQRCFile): 253 | argList, commandString = _buildCommand(module, command, options, sourceFilename, destFilename) 254 | 255 | commandResult = subprocess.run(argList, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 256 | 257 | if commandResult.returncode == 0: 258 | click.secho(commandString, fg='green') 259 | else: 260 | if commandResult.stderr: 261 | click.secho(commandString, fg='yellow') 262 | click.secho(commandResult.stderr.decode(), fg='red') 263 | else: 264 | click.secho(commandString, fg='yellow') 265 | click.secho('Command returned with non-zero exit status %i' % commandResult.returncode, 266 | fg='red') 267 | else: 268 | click.secho('Skipping %s, up to date' % filename) 269 | 270 | if not foundItem: 271 | click.secho('No items found in %s' % sourceFileExpr) 272 | 273 | 274 | if __name__ == '__main__': 275 | cli() 276 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from setuptools import setup 5 | 6 | currentPath = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | 9 | def find_version(filename): 10 | with open(filename, 'r') as fh: 11 | # Read first 2048 bytes, __version__ string will be within that 12 | data = fh.read(2048) 13 | 14 | match = re.search(r'^__version__ = [\'"]([\w\d.\-]*)[\'"]$', data, re.M) 15 | 16 | if match: 17 | return match.group(1) 18 | 19 | raise RuntimeError('Unable to find version string.') 20 | 21 | 22 | # Get the long description from the README file 23 | with open(os.path.join(currentPath, 'README.md'), 'r') as f: 24 | longDescription = f.read() 25 | 26 | longDescription = '\n' + longDescription 27 | 28 | REQUIREMENTS = { 29 | 'core': [ 30 | 'PyQt5', 31 | 'click', 32 | 'pyyaml', 33 | ], 34 | 'test': [ 35 | 'pytest', 36 | 'pytest-cov', 37 | ], 38 | 'dev': [ 39 | # 'requirement-for-development-purposes-only', 40 | ], 41 | 'doc': [ 42 | ], 43 | } 44 | 45 | 46 | setup(name='pyqt5ac', 47 | version=find_version('pyqt5ac.py'), 48 | description='Python module to automatically compile UI and RC files in PyQt5 to Python files', 49 | long_description=longDescription, 50 | long_description_content_type='text/markdown', 51 | author='Addison Elliott', 52 | author_email='addison.elliott@gmail.com', 53 | url='https://github.com/addisonElliott/pyqt5ac', 54 | license='MIT License', 55 | install_requires=REQUIREMENTS['core'], 56 | extras_require={ 57 | **REQUIREMENTS, 58 | # The 'dev' extra is the union of 'test' and 'doc', with an option 59 | # to have explicit development dependencies listed. 60 | 'dev': [req 61 | for extra in ['dev', 'test', 'doc'] 62 | for req in REQUIREMENTS.get(extra, [])], 63 | # The 'all' extra is the union of all requirements. 64 | 'all': [req for reqs in REQUIREMENTS.values() for req in reqs], 65 | }, 66 | python_requires='>=3', 67 | py_modules=['pyqt5ac'], 68 | entry_points={ 69 | 'console_scripts': ['pyqt5ac = pyqt5ac:cli'] 70 | }, 71 | keywords='pyqt pyqt5 qt qt5 qt auto compile generate ui rc pyuic5 pyrcc5 resource designer creator automatic', 72 | classifiers=[ 73 | 'License :: OSI Approved :: MIT License', 74 | 'Topic :: Scientific/Engineering', 75 | 'Programming Language :: Python', 76 | 'Programming Language :: Python :: 3', 77 | 'Programming Language :: Python :: 3.5', 78 | 'Programming Language :: Python :: 3.6', 79 | 'Programming Language :: Python :: 3.7', 80 | 'Programming Language :: Python :: 3.8' 81 | ], 82 | project_urls={ 83 | 'Source': 'https://github.com/addisonElliott/pyqt5ac', 84 | 'Tracker': 'https://github.com/addisonElliott/pyqt5ac/issues', 85 | } 86 | ) 87 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/pyqt5ac/5945a85f6230a97f3467c79441d87f5f9cdb478d/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_pyqt5ac.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import pytest 5 | 6 | from .. import pyqt5ac 7 | 8 | 9 | def _is_gitlab_ci(): 10 | return os.getenv("GITLAB_CI") is not None 11 | 12 | 13 | def _assert_path_exists(expected_path): 14 | assert expected_path.check(), ("Generated file does not exist " + str(expected_path)) 15 | 16 | 17 | def _assert_path_does_not_exist(expected_path): 18 | assert not expected_path.check(), ("Generated file exists " + str(expected_path)) 19 | 20 | 21 | def _assert_empty_file_exists(empty_file): 22 | _assert_path_exists(empty_file) 23 | assert "" == empty_file.read() 24 | 25 | 26 | def _wait(): 27 | if _is_gitlab_ci(): 28 | time.sleep(1) 29 | else: 30 | time.sleep(0.010) 31 | 32 | 33 | def _write_config_file(dir): 34 | config = dir.join("input_config.yml") 35 | config.write("""ioPaths: 36 | - 37 | - '{dir}/gui/*.ui' 38 | - '{dir}/generated/%%FILENAME%%_ui.py' 39 | - 40 | - '{dir}/resources/*.qrc' 41 | - '{dir}/generated/%%FILENAME%%_rc.py'""".format(dir=str(dir))) 42 | 43 | return config 44 | 45 | 46 | def _write_config_file_with_variables(dir, variable_name, variable_value): 47 | config = dir.join("input_config.yml") 48 | config.write("""variables: 49 | {variable_name}: {variable_value} 50 | ioPaths: 51 | - 52 | - '%%{variable_name}%%/gui/*.ui' 53 | - '%%{variable_name}%%/generated/%%FILENAME%%_ui.py' 54 | - 55 | - '%%{variable_name}%%/resources/*.qrc' 56 | - '%%{variable_name}%%/generated/%%FILENAME%%_rc.py'""".format(variable_name=variable_name, 57 | variable_value=variable_value)) 58 | 59 | return config 60 | 61 | 62 | def _write_ui_file(file): 63 | file.write(""" 64 | 65 | MainWidget 66 | 67 | 68 | 69 | 0 70 | 0 71 | 675 72 | 591 73 | 74 | 75 | 76 | 77 | """) 78 | 79 | 80 | def _write_resource_file(file): 81 | file.write(""" 82 | 83 | example.png 84 | 85 | """) 86 | 87 | 88 | def test_import_module(): 89 | assert pyqt5ac.__version__ is not None 90 | 91 | 92 | def test_without_generation(tmpdir): 93 | config = _write_config_file(tmpdir) 94 | 95 | pyqt5ac.main(config=str(config)) 96 | 97 | assert not tmpdir.join("generated").check() 98 | 99 | 100 | def test_ui_generation(tmpdir): 101 | config = _write_config_file(tmpdir) 102 | ui_file = tmpdir.mkdir("gui").join("main.ui") 103 | _write_ui_file(ui_file) 104 | 105 | pyqt5ac.main(config=str(config), uicOptions="-d") 106 | 107 | _assert_path_exists(tmpdir.join("generated")) 108 | _assert_path_exists(tmpdir.join("generated/main_ui.py")) 109 | _assert_empty_file_exists(tmpdir.join("generated/__init__.py")) 110 | 111 | 112 | def test_resource_generation(tmpdir): 113 | config = _write_config_file(tmpdir) 114 | resource_file = tmpdir.mkdir("resources").join("resource.qrc") 115 | _write_resource_file(resource_file) 116 | 117 | example_image = tmpdir.join("resources/example.png") 118 | example_image.write("test") 119 | 120 | pyqt5ac.main(config=str(config)) 121 | 122 | _assert_path_exists(tmpdir.join("generated")) 123 | _assert_path_exists(tmpdir.join("generated/resource_rc.py")) 124 | _assert_empty_file_exists(tmpdir.join("generated/__init__.py")) 125 | 126 | 127 | def test_ui_generation_when_up_to_date(tmpdir): 128 | config = _write_config_file(tmpdir) 129 | ui_file = tmpdir.mkdir("gui").join("main.ui") 130 | _write_ui_file(ui_file) 131 | 132 | dest_file = tmpdir.mkdir("generated").join("main_ui.py") 133 | dest_file.write("test") 134 | modification_time = dest_file.mtime() 135 | 136 | pyqt5ac.main(config=str(config)) 137 | 138 | _assert_path_exists(tmpdir.join("generated")) 139 | _assert_empty_file_exists(tmpdir.join("generated/__init__.py")) 140 | dest_file = tmpdir.join("generated/main_ui.py") 141 | _assert_path_exists(dest_file) 142 | assert modification_time == dest_file.mtime() 143 | assert "test" == dest_file.read() 144 | 145 | 146 | def test_ui_generation_when_out_of_date(tmpdir): 147 | config = _write_config_file(tmpdir) 148 | dest_file = tmpdir.mkdir("generated").join("main_ui.py") 149 | dest_file.write("test") 150 | dest_mod_time = dest_file.mtime() 151 | 152 | _wait() 153 | ui_file = tmpdir.mkdir("gui").join("main.ui") 154 | _write_ui_file(ui_file) 155 | source_mod_time = ui_file.mtime() 156 | 157 | assert source_mod_time > dest_mod_time 158 | 159 | pyqt5ac.main(config=str(config)) 160 | 161 | _assert_path_exists(tmpdir.join("generated")) 162 | _assert_empty_file_exists(tmpdir.join("generated/__init__.py")) 163 | dest_file = tmpdir.join("generated/main_ui.py") 164 | _assert_path_exists(dest_file) 165 | assert dest_mod_time != dest_file.mtime() 166 | assert "test" != dest_file.read() 167 | 168 | 169 | def test_resource_generation_when_up_to_date(tmpdir): 170 | config = _write_config_file(tmpdir) 171 | resource_file = tmpdir.mkdir("resources").join("resource.qrc") 172 | _write_resource_file(resource_file) 173 | 174 | example_image = tmpdir.join("resources/example.png") 175 | example_image.write("test") 176 | 177 | dest_file = tmpdir.mkdir("generated").join("main_rc.py") 178 | dest_file.write("test") 179 | modification_time = dest_file.mtime() 180 | 181 | pyqt5ac.main(config=str(config)) 182 | 183 | _assert_path_exists(tmpdir.join("generated")) 184 | _assert_empty_file_exists(tmpdir.join("generated/__init__.py")) 185 | dest_file = tmpdir.join("generated/main_rc.py") 186 | _assert_path_exists(dest_file) 187 | assert modification_time == dest_file.mtime() 188 | assert "test" == dest_file.read() 189 | 190 | 191 | def test_resource_generation_when_resource_out_of_date(tmpdir): 192 | config = _write_config_file(tmpdir) 193 | tmpdir.mkdir("resources") 194 | example_image = tmpdir.join("resources/example.png") 195 | example_image.write("test") 196 | 197 | dest_file = tmpdir.mkdir("generated").join("resource_rc.py") 198 | dest_file.write("test") 199 | dest_mod_time = dest_file.mtime() 200 | 201 | _wait() 202 | resource_file = tmpdir.join("resources/resource.qrc") 203 | _write_resource_file(resource_file) 204 | source_mod_time = resource_file.mtime() 205 | 206 | assert source_mod_time > dest_mod_time 207 | pyqt5ac.main(config=str(config)) 208 | 209 | _assert_path_exists(tmpdir.join("generated")) 210 | _assert_empty_file_exists(tmpdir.join("generated/__init__.py")) 211 | dest_file = tmpdir.join("generated/resource_rc.py") 212 | _assert_path_exists(dest_file) 213 | assert dest_mod_time != dest_file.mtime() 214 | assert "test" != dest_file.read() 215 | 216 | 217 | def test_resource_generation_when_image_out_of_date(tmpdir): 218 | config = _write_config_file(tmpdir) 219 | tmpdir.mkdir("resources") 220 | resource_file = tmpdir.join("resources/resource.qrc") 221 | _write_resource_file(resource_file) 222 | 223 | dest_file = tmpdir.mkdir("generated").join("resource_rc.py") 224 | dest_file.write("test") 225 | dest_mod_time = dest_file.mtime() 226 | 227 | _wait() 228 | example_image = tmpdir.join("resources/example.png") 229 | example_image.write("test") 230 | 231 | pyqt5ac.main(config=str(config)) 232 | 233 | _assert_path_exists(tmpdir.join("generated")) 234 | _assert_empty_file_exists(tmpdir.join("generated/__init__.py")) 235 | dest_file = tmpdir.join("generated/resource_rc.py") 236 | _assert_path_exists(dest_file) 237 | assert dest_mod_time != dest_file.mtime() 238 | assert "test" != dest_file.read() 239 | 240 | 241 | def test_generation_fails_with_forbidden_filename_variable(tmpdir): 242 | run_forbidden_variable_case(tmpdir, 'FILENAME') 243 | 244 | 245 | def test_generation_fails_with_forbidden_ext_variable(tmpdir): 246 | run_forbidden_variable_case(tmpdir, 'EXT') 247 | 248 | 249 | def test_generation_fails_with_forbidden_dirname_variable(tmpdir): 250 | run_forbidden_variable_case(tmpdir, 'DIRNAME') 251 | 252 | 253 | def run_forbidden_variable_case(tmpdir, variable): 254 | config = _write_config_file_with_variables(tmpdir, variable, 'value') 255 | 256 | with pytest.raises(ValueError): 257 | pyqt5ac.main(config=str(config)) 258 | 259 | 260 | def test_ui_generation_when_invalid(tmpdir): 261 | config = _write_config_file(tmpdir) 262 | ui_file = tmpdir.mkdir("gui").join("main.ui") 263 | ui_file.write("invalid_content") 264 | 265 | pyqt5ac.main(config=str(config)) 266 | 267 | assert tmpdir.join("generated").check() 268 | _assert_empty_file_exists(tmpdir.join("generated/__init__.py")) 269 | # TODO generated file should not exist and pyqt5ac should fail 270 | assert tmpdir.join("generated/main_ui.py").check() 271 | 272 | 273 | def test_ui_generation_with_variables(tmpdir): 274 | config = _write_config_file_with_variables(tmpdir, 'BASENAME', str(tmpdir)) 275 | ui_file = tmpdir.mkdir("gui").join("main.ui") 276 | _write_ui_file(ui_file) 277 | 278 | pyqt5ac.main(config=str(config), uicOptions="-d") 279 | 280 | _assert_path_exists(tmpdir.join("generated")) 281 | _assert_empty_file_exists(tmpdir.join("generated/__init__.py")) 282 | _assert_path_exists(tmpdir.join("generated/main_ui.py")) 283 | 284 | 285 | def test_init_is_untouched(tmpdir): 286 | config = _write_config_file(tmpdir) 287 | ui_file = tmpdir.mkdir("gui").join("main.ui") 288 | _write_ui_file(ui_file) 289 | init_file = tmpdir.mkdir("generated").join("__init__.py") 290 | init_file.write("test") 291 | 292 | pyqt5ac.main(config=str(config)) 293 | 294 | _assert_path_exists(tmpdir.join("generated")) 295 | _assert_path_exists(tmpdir.join("generated/main_ui.py")) 296 | _assert_path_exists(tmpdir.join("generated/__init__.py")) 297 | assert "test" == init_file.read() 298 | 299 | 300 | def test_dont_check_for_init(tmpdir): 301 | config = _write_config_file(tmpdir) 302 | ui_file = tmpdir.mkdir("gui").join("main.ui") 303 | _write_ui_file(ui_file) 304 | 305 | pyqt5ac.main(config=str(config), initPackage=False) 306 | 307 | _assert_path_exists(tmpdir.join("generated")) 308 | _assert_path_exists(tmpdir.join("generated/main_ui.py")) 309 | _assert_path_does_not_exist(tmpdir.join("generated/__init__.py")) 310 | 311 | 312 | def test_relative_paths_are_relative_to_their_config_file(tmpdir): 313 | config = _write_config_file(tmpdir) 314 | ui_file = tmpdir.mkdir("gui").join("main.ui") 315 | _write_ui_file(ui_file) 316 | tmpdir.mkdir("another_directory") 317 | os.chdir(str(tmpdir.join("another_directory"))) # Python3.5 compatibility 318 | 319 | pyqt5ac.main(config=str(config)) 320 | 321 | _assert_path_exists(tmpdir.join("generated")) 322 | _assert_path_exists(tmpdir.join("generated/main_ui.py")) 323 | 324 | 325 | def test_relative_paths_are_relative_to_cwd_if_no_config_file_is_given(tmpdir): 326 | ui_file = tmpdir.mkdir("another_directory").mkdir("gui").join("main.ui") 327 | _write_ui_file(ui_file) 328 | os.chdir(str(tmpdir.join("another_directory"))) # Python3.5 compatibility 329 | 330 | pyqt5ac.main(uicOptions='--from-imports', force=False, initPackage=True, ioPaths=[ 331 | ['gui/*.ui', 'generated/%%FILENAME%%_ui.py'], 332 | ]) 333 | 334 | _assert_path_does_not_exist(tmpdir.join("generated")) 335 | _assert_path_does_not_exist(tmpdir.join("generated/main_ui.py")) 336 | _assert_path_exists(tmpdir.join("another_directory").join("generated")) 337 | _assert_path_exists(tmpdir.join("another_directory").join("generated/main_ui.py")) 338 | 339 | 340 | def test_absolute_paths_stay_untouched(tmpdir): 341 | dir1 = tmpdir.mkdir("dir1") 342 | dir2 = tmpdir.mkdir("dir2") 343 | dir3 = tmpdir.mkdir("dir3") 344 | 345 | ui_file = dir2.mkdir("gui").join("main.ui") 346 | _write_ui_file(ui_file) 347 | 348 | os.chdir(str(dir1)) # Python3.5 compatibility 349 | pyqt5ac.main(uicOptions='--from-imports', force=False, initPackage=True, ioPaths=[ 350 | [str(dir2.join('gui/*.ui')), str(dir3.join('generated/%%FILENAME%%_ui.py'))], 351 | ]) 352 | 353 | _assert_path_does_not_exist(tmpdir.join("generated")) 354 | _assert_path_does_not_exist(tmpdir.join("generated/main_ui.py")) 355 | _assert_path_exists(dir3.join("generated")) 356 | _assert_path_exists(dir3.join("generated/main_ui.py")) 357 | 358 | 359 | def test_config_file_path_is_relative_to_cwd(tmpdir): 360 | another_dir = tmpdir.mkdir("another_directory") 361 | _write_config_file(another_dir) 362 | ui_file = another_dir.mkdir("gui").join("main.ui") 363 | _write_ui_file(ui_file) 364 | 365 | os.chdir(str(another_dir)) # Python3.5 compatibility 366 | pyqt5ac.main(config="input_config.yml") 367 | 368 | _assert_path_exists(another_dir.join("generated")) 369 | _assert_path_exists(another_dir.join("generated/main_ui.py")) 370 | 371 | os.chdir(str(tmpdir)) # Python3.5 compatibility 372 | with pytest.raises(FileNotFoundError): 373 | pyqt5ac.main(config="input_config.yml") 374 | 375 | 376 | --------------------------------------------------------------------------------