Skip to content

Commit

Permalink
Merge pull request #8 from domarm-comat/pyside6-rcc-support
Browse files Browse the repository at this point in the history
feat: add pyside6-rcc support
  • Loading branch information
domarm-comat authored Nov 21, 2024
2 parents 13228b5 + a098b6b commit 06696d2
Show file tree
Hide file tree
Showing 15 changed files with 1,093 additions and 333 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/code-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3
Expand Down
51 changes: 43 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@

![GitHub_repo](https://img.shields.io/github/license/domarm-comat/pyqt6rc?style=for-the-badge)

This library is providing scripts to convert resource paths in files generated by pyuic6 command.
PyQt6 does not provide pyrcc6 script to convert resources, that's the purpose of this package.
In current PyQt6 implementation, py files created by pyuic6 script has wrong resource path.
This library offers scripts to correct resource paths in files generated by the `pyuic6` command.
Since PyQt6 does not include the `pyrcc6` script for converting resources, this package serves that purpose.
In the current PyQt6 implementation, the .py files created by the pyuic6 command contain incorrect resource paths.

**UPDATE:** As of `PyQt-6.3.1` it's possible to use `Qt’s` resource system again.
We can use `pyside6-rcc` to convert `rcc` files into `py`.
Source for this solution can be found in [StackOverflow answer](https://stackoverflow.com/a/66104738/17872926).

Provided scripts are converting .ui files into .py files using three different ways:

* Using `pyside6-rcc` script to convert `rcc` files into `py` and `pyuic6` to convert all `ui` files [**Use pyside6rc**]
* Native >= python3.7 solution
using [importlib](https://docs.python.org/3/library/importlib.html#module-importlib.resources) [**Use pyqt6rc**].
* Use of [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/), for compatibility with
Python3.6+ [**Use pyqt6rc with -c option**]
* Adding resource search path using QtCore.QDir.addSearchPath() and modifying generated prefixes [**Use pyqt6sp**]
* Adding resource search path using `QtCore.QDir.addSearchPath()` and modifying generated prefixes [**Use pyqt6sp**]

More on this topic can be found on [StackOverflow](https://stackoverflow.com/questions/66099225/how-can-resources-be-provided-in-pyqt6-which-has-no-pyrcc).

Expand All @@ -21,14 +26,20 @@ parent folders and looking for `\_\_init\_\_.py` file.

# Conversion #

Generated template using pyuic6 script:
Generated template using `pyside6rc` script:

```python
import myPackage.resources.icons # noqa
```

Generated template using `pyuic6` script:

```python
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/icons/icon1.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
```

Generated template using pyqt6rc script:
Generated template using `pyqt6rc` script:

```python
from importlib.resources import path
Expand All @@ -38,7 +49,7 @@ with path("myPackage.resources.icons", "icon1.png") as f_path:
icon.addPixmap(QtGui.QPixmap(str(f_path)), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
```

Generated template using pyqt6rc (-c, --compatible) script:
Generated template using `pyqt6rc` (-c, --compatible) script:

```python
from importlib_resources import path
Expand All @@ -48,7 +59,7 @@ with path("myPackage.resources.icons", "icon1.png") as f_path:
icon.addPixmap(QtGui.QPixmap(str(f_path)), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
```

Generated template using pyqt6sp script:
Generated template using `pyqt6sp` script:

```python
import os
Expand Down Expand Up @@ -90,6 +101,30 @@ myPackage
│ template2.ui
```

Batch convert all .ui files located in the templates directory using `pyside6-rcc`

```shell
pyside6rc /myPackage/templates
```

Batch convert all .ui files located in the templates directory using `pyside6-rcc`
Without writing import line of resource package to `ui` files (`-niw`)
```shell
pyside6rc -niw /myPackage/templates
```

Batch convert all .ui files located in the templates directory using `pyside6-rcc`
Without `ui` to `py` conversion (`-npc`)
```shell
pyside6rc -npc /myPackage/templates
```

Batch convert all .ui files located in the templates directory using `pyside6-rcc`

```shell
pyside6-rcc /myPackage/templates
```

Batch convert all .ui files located in the templates directory

```shell
Expand Down
623 changes: 350 additions & 273 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyqt6rc"
version = "0.6.1"
version = "0.7.0"
description = "PyQt6 UI templates resource converter"
authors = ["Martin Domaracký <domarm@comat.sk>"]
license = "MIT"
Expand All @@ -18,9 +18,10 @@ classifiers = [
]

[tool.poetry.dependencies]
python = ">=3.8,<3.13"
python = ">=3.9,<3.13"
importlib-resources = "^5.12.0"
pyqt6 = "^6.5.0"
pyside6 = "^6.8.0.2"

[tool.poetry.group.dev.dependencies]
pytest = "^7.3.1"
Expand All @@ -32,6 +33,7 @@ pre-commit = "^3.5.0"
[tool.poetry.scripts]
pyqt6rc = "pyqt6rc.scripts.pyqt6rc:run"
pyqt6sp = "pyqt6rc.scripts.pyqt6sp:run"
pyside6rc = "pyqt6rc.scripts.pyside6rc:run"

[build-system]
requires = ["poetry-core"]
Expand Down
2 changes: 1 addition & 1 deletion pyqt6rc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re

__version__ = "0.4.0"
__version__ = "0.7.0"

resource_pattern = re.compile(r'":(\/.*?\.[\w:]+)"')
indent_pattern = re.compile(r"\s+")
65 changes: 64 additions & 1 deletion pyqt6rc/convert_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import xml.etree.ElementTree as Et
from os.path import dirname, basename
from pathlib import Path
from typing import List, Optional, Dict, Any
from pyqt6rc import resource_pattern, indent_pattern

Expand Down Expand Up @@ -52,14 +53,47 @@ def parse_qrc(qrc_file: str) -> Dict[str, Any]:
if not prefix.endswith("/"):
prefix += "/"

basename = os.path.basename(qrc_file)
resources[prefix] = {
"module_path": get_module_path(os.path.dirname(qrc_file)),
"aliases": aliases,
"path": qrc_file,
"basename": basename,
"import_as": Path(basename).stem,
}
return resources


def update_resources(ui_file: str, resources: Dict[str, Any]) -> str:
def update_resources(
ui_file: str, resources: Dict[str, Any]
) -> Dict[str, List[Dict[str, Any]]]:
"""
Read ui file and collect all input resource files.
:param str ui_file: input ui template
:param dict resources: input parsed resources
:return:
"""
tree = Et.parse(ui_file)
root = tree.getroot()

if root.tag != "ui":
raise Exception("Invalid template file format.")

ui_dir = os.path.dirname(ui_file)
current_resources: Dict[str, List[Dict[str, Any]]] = {"qrc_info": []}
for child in root:
if child.tag == "resources":
for include in child:
location = include.attrib.get("location", None)
if location is not None:
resource_location = os.path.normpath(os.path.join(ui_dir, location))
qrc_info = parse_qrc(resource_location)
resources.update(qrc_info)
current_resources["qrc_info"].append(qrc_info)
return current_resources


def update_resources_sp(ui_file: str, resources: Dict[str, Any]) -> str:
"""
Read ui file and collect all input resource files.
:param str ui_file: input ui template
Expand All @@ -84,6 +118,35 @@ def update_resources(ui_file: str, resources: Dict[str, Any]) -> str:
return dirname(location) if location is not None else ""


def qrc_to_py(qrc_file: str) -> str:
return os.popen(f"pyside6-rcc {qrc_file}").read()


def pyside6_qrc_to_pyqt6(qrc_input: str) -> str:
return qrc_input.replace("PySide6", "PyQt6")


def save_rcc_py(qrc_file: str, py_input: str) -> None:
"""
Save python template into file.
Use ui filename and change .ui suffix to .py.
If output_dir is None, use same dir as a .ui template file to store converted .py template.
:param str qrc_file: input ui template file
:param str py_input: converted python template
:param str output_dir: output directory
:return: None
"""
input_filename = os.path.basename(qrc_file)
parts = input_filename.split(".")
parts[-1] = "py"
output_dir = os.path.dirname(qrc_file)
output_filename = ".".join(parts)
output_filename_path = os.path.join(output_dir, output_filename)
with open(output_filename_path, "w") as fp:
fp.write(py_input)
logging.info(f"{input_filename} > {output_filename}")


def ui_to_py(ui_file: str) -> str:
"""
Use pyuic6 to convert ui template into py file
Expand Down
3 changes: 2 additions & 1 deletion pyqt6rc/script_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
def set_logger(disabled: bool = False) -> None:
logging.basicConfig(format="%(levelname)s - %(message)s", level=logging.INFO)
logger = logging.getLogger("main")
logger.info("sadads")
logger.disabled = disabled
if disabled:
logging.disable(logging.INFO)
6 changes: 3 additions & 3 deletions pyqt6rc/scripts/pyqt6rc.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@
def run() -> None:
resources: Dict[str, Any] = {}
for input_file in input_files:
resources_found = update_resources(input_file, resources)
current_resources = update_resources(input_file, resources)
py_input = ui_to_py(input_file)
# Skip conversion if no resource were found in input template file
if resources_found is not None:
# Do conversion only when resources are found in current ui file
if current_resources["qrc_info"]:
py_input = modify_py(py_input, resources, args.tab_size, args.compatible)
save_py(input_file, py_input, args.out)
4 changes: 2 additions & 2 deletions pyqt6rc/scripts/pyqt6sp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
ui_to_py,
save_py,
get_ui_files,
update_resources,
modify_py_sp,
update_resources_sp,
)
from pyqt6rc.script_helpers import set_logger

Expand Down Expand Up @@ -84,7 +84,7 @@
def run() -> None:
for input_file in input_files:
resources: Dict[str, Any] = {}
resource_rel_path = update_resources(input_file, resources)
resource_rel_path = update_resources_sp(input_file, resources)
py_input = ui_to_py(input_file)
py_input = modify_py_sp(py_input, resources, resource_rel_path, args.tab_size)
save_py(input_file, py_input, args.out)
103 changes: 103 additions & 0 deletions pyqt6rc/scripts/pyside6rc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import argparse
import os
import sys
from typing import Dict, Any

from pyqt6rc import __version__
from pyqt6rc.convert_tools import (
ui_to_py,
save_py,
get_ui_files,
update_resources,
qrc_to_py,
save_rcc_py,
pyside6_qrc_to_pyqt6,
)
from pyqt6rc.script_helpers import set_logger

description = [
f"pyqt6rc v{__version__}",
"PyQt6 UI templates - Resource Converter.",
"Default input location is Current Working Directory.",
"",
"Usage examples:",
" Convert all .ui files in CWD:",
" pyside6rc",
"",
" Convert all .ui files in CWD, save output in different directory:",
" pyside6rc -o /directory/with/converted/templates",
"",
]

arguments = sys.argv
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter, description="\r\n".join(description)
)
parser.add_argument(
"input",
type=str,
help="Path to .ui template file or Directory containing .ui files."
"If empty, scan current working directory and use all .ui template files.",
default="*",
nargs="?",
)
parser.add_argument(
"-o",
"--out",
type=str,
help="Output directory to save converted templates",
default=None,
)
parser.add_argument("-s", "--silent", help="Supress logging", action="store_true")
parser.add_argument(
"-npc",
"--no-pyuic6-conversion",
help="Don't convert .ui files to .py files.",
action="store_true",
)
parser.add_argument(
"-niw",
"--no-import-write",
help="Don't write import to converted ui files.",
action="store_true",
)
args = parser.parse_args()

# Set logger
set_logger(args.silent)

# Input files check
if args.input == "*":
input_files = get_ui_files(os.getcwd())
elif os.path.isdir(args.input):
input_files = get_ui_files(args.input)
else:
if not args.input.endswith(".ui"):
raise Exception(f"Not template file {args.input}.")
if not os.path.exists(args.input):
raise Exception(f"Template file {args.input} does not exists.")
input_files = [args.input]


def run() -> None:
resources: Dict[str, Any] = {}
converted_qrcs = []
for input_file in input_files:
current_resources = update_resources(input_file, resources)
py_input = ui_to_py(input_file)
injected_imports = []
for qrc_info in current_resources["qrc_info"]:
for info in qrc_info.values():
if info["path"] not in converted_qrcs:
# Only do conversion once for same qrc file
qrc_input = qrc_to_py(info["path"])
qrc_input = pyside6_qrc_to_pyqt6(qrc_input)
converted_qrcs.append(info["path"])
save_rcc_py(info["path"], qrc_input)
import_qrc = f"import {info['module_path']}.{info['import_as']}"
if not args.no_import_write and import_qrc not in injected_imports:
# Only inject qrc import once
injected_imports.append(import_qrc)
py_input += "\n" + import_qrc + " # noqa"
if not args.no_pyuic6_conversion:
save_py(input_file, py_input, args.out)
Loading

0 comments on commit 06696d2

Please sign in to comment.