├── .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 | [](https://github.com/addisonElliott/pyqt5ac/actions)
2 | [](https://pypi.org/project/pyqt5ac/)
3 | [](https://pypi.org/project/pyqt5ac/)
4 | [](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 |
--------------------------------------------------------------------------------