From 0c78c7ad8e82af91d7def143a77d2b209636143d Mon Sep 17 00:00:00 2001 From: fedepell Date: Sat, 23 Jul 2016 12:51:04 +0200 Subject: [PATCH 1/7] First version of the pyqt5 extra to add QT5 ui/resources translation to py to be used with pyqt5 or pyside2 --- waflib/extras/pyqt5.py | 280 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 waflib/extras/pyqt5.py diff --git a/waflib/extras/pyqt5.py b/waflib/extras/pyqt5.py new file mode 100644 index 00000000..d82048f4 --- /dev/null +++ b/waflib/extras/pyqt5.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python +# encoding: utf-8 +# Thomas Nagy, 2006-2016 (ita) original C++ QT5 implementation +# Federico Pellegrin, 2016 (fedepell) adapted for Python + +""" +This tool helps with finding Python Qt5 tools and libraries, +and provides translation from QT5 files to Python code. + +The following snippet illustrates the tool usage:: + + def options(opt): + opt.load('py pyqt5') + + def configure(conf): + conf.load('py pyqt5') + + def build(bld): + bld( + features = 'py pyqt5', + source = 'main.py textures.qrc aboutDialog.ui', + ) + +Here, the UI description and resource files will be processed +to generate code. + +Usage +===== + +Load the "pyqt5" tool. + +Reference the qrc resources files or ui5 definition files as +your sources and they will be translated into python code with +the system tools (PyQt5 or pyside2 are searched in this order) +and then compiled +""" + +try: + from xml.sax import make_parser + from xml.sax.handler import ContentHandler +except ImportError: + has_xml = False + ContentHandler = object +else: + has_xml = True + +import os, sys +from waflib.Tools import python +from waflib import Task, Utils, Options, Errors, Context +from waflib.TaskGen import feature, after_method, extension, before_method +from waflib.Configure import conf +from waflib import Logs + +EXT_RCC = ['.qrc'] +""" +File extension for the resource (.qrc) files +""" + +EXT_UI = ['.ui'] +""" +File extension for the user interface (.ui) files +""" + + +class XMLHandler(ContentHandler): + """ + Parses ``.qrc`` files + """ + def __init__(self): + self.buf = [] + self.files = [] + def startElement(self, name, attrs): + if name == 'file': + self.buf = [] + def endElement(self, name): + if name == 'file': + self.files.append(str(''.join(self.buf))) + def characters(self, cars): + self.buf.append(cars) + +@extension(*EXT_RCC) +def create_pyrcc_task(self, node): + "Creates rcc and py task for ``.qrc`` files" + rcnode = node.change_ext('.py') + self.create_task('pyrcc', node, rcnode) + self.process_py(rcnode) + +@extension(*EXT_UI) +def create_pyuic_task(self, node): + "Create uic tasks and py for user interface ``.ui`` definition files" + uinode = node.change_ext('.py') + self.create_task('ui5py', node, uinode) + self.process_py(uinode) + +@extension('.ts') +def add_pylang(self, node): + """Adds all the .ts file into ``self.lang``""" + self.lang = self.to_list(getattr(self, 'lang', [])) + [node] + +@feature('pyqt5') +@after_method('apply_link') +def apply_pyqt5(self): + """ + The additional parameters are: + + :param lang: list of translation files (\*.ts) to process + :type lang: list of :py:class:`waflib.Node.Node` or string without the .ts extension + :param langname: if given, transform the \*.ts files into a .qrc files to include in the binary file + :type langname: :py:class:`waflib.Node.Node` or string without the .qrc extension + """ + if getattr(self, 'lang', None): + qmtasks = [] + for x in self.to_list(self.lang): + if isinstance(x, str): + x = self.path.find_resource(x + '.ts') + qmtasks.append(self.create_task('ts2qm', x, x.change_ext('.qm'))) + + + if getattr(self, 'langname', None): + qmnodes = [x.outputs[0] for x in qmtasks] + rcnode = self.langname + if isinstance(rcnode, str): + rcnode = self.path.find_or_declare(rcnode + '.qrc') + t = self.create_task('qm2rcc', qmnodes, rcnode) + k = create_pyrcc_task(self, t.outputs[0]) + + +class pyrcc(Task.Task): + """ + Processes ``.qrc`` files + """ + color = 'BLUE' + run_str = '${QT_PYRCC} ${SRC} -o ${TGT}' + ext_out = ['.py'] + + + def rcname(self): + return os.path.splitext(self.inputs[0].name)[0] + + def scan(self): + """Parse the *.qrc* files""" + if not has_xml: + Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!') + return ([], []) + + parser = make_parser() + curHandler = XMLHandler() + parser.setContentHandler(curHandler) + fi = open(self.inputs[0].abspath(), 'r') + try: + parser.parse(fi) + finally: + fi.close() + + nodes = [] + names = [] + root = self.inputs[0].parent + for x in curHandler.files: + nd = root.find_resource(x) + if nd: nodes.append(nd) + else: names.append(x) + return (nodes, names) + + +class ui5py(Task.Task): + """ + Processes ``.ui`` files for python + """ + color = 'BLUE' + run_str = '${QT_PYUIC} ${SRC} -o ${TGT}' + ext_out = ['.py'] + +class ts2qm(Task.Task): + """ + Generates ``.qm`` files from ``.ts`` files + """ + color = 'BLUE' + run_str = '${QT_LRELEASE} ${QT_LRELEASE_FLAGS} ${SRC} -qm ${TGT}' + +class qm2rcc(Task.Task): + """ + Generates ``.qrc`` files from ``.qm`` files + """ + color = 'BLUE' + after = 'ts2qm' + def run(self): + """Create a qrc file including the inputs""" + txt = '\n'.join(['%s' % k.path_from(self.outputs[0].parent) for k in self.inputs]) + code = '\n\n%s\n\n' % txt + self.outputs[0].write(code) + +def configure(self): + """ + Besides the configuration options, the environment variable QT5_ROOT may be used + to give the location of the qt5 libraries (absolute path). + + The detection uses the program ``pkg-config`` through :py:func:`waflib.Tools.config_c.check_cfg` + """ + + self.find_pyqt5_binaries() + + # warn about this during the configuration too + if not has_xml: + Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!') + + + +@conf +def find_pyqt5_binaries(self): + """ + Detects Qt programs such as qmake, moc, uic, lrelease + """ + env = self.env + opt = Options.options + + qtdir = getattr(opt, 'qtdir', '') + qtbin = getattr(opt, 'qtbin', '') + + paths = [] + + if qtdir: + qtbin = os.path.join(qtdir, 'bin') + + # the qt directory has been given from QT5_ROOT - deduce the qt binary path + if not qtdir: + qtdir = self.environ.get('QT5_ROOT', '') + qtbin = self.environ.get('QT5_BIN') or os.path.join(qtdir, 'bin') + + if qtbin: + paths = [qtbin] + + # no qtdir, look in the path and in /usr/local/Trolltech + if not qtdir: + paths = self.environ.get('PATH', '').split(os.pathsep) + paths.extend(['/usr/share/qt5/bin', '/usr/local/lib/qt5/bin']) + try: + lst = Utils.listdir('/usr/local/Trolltech/') + except OSError: + pass + else: + if lst: + lst.sort() + lst.reverse() + + # keep the highest version + qtdir = '/usr/local/Trolltech/%s/' % lst[0] + qtbin = os.path.join(qtdir, 'bin') + paths.append(qtbin) + + + def find_bin(lst, var): + if var in env: + return + for f in lst: + try: + ret = self.find_program(f, path_list=paths) + except self.errors.ConfigurationError: + pass + else: + env[var]=ret + break + + find_bin(['pyuic5','pyside2-uic'], 'QT_PYUIC') + if not env.QT_PYUIC: + self.fatal('cannot find the uic compiler for python for qt5') + + find_bin(['pyrcc5','pyside2-rcc'], 'QT_PYRCC') + if not env.QT_PYUIC: + self.fatal('cannot find the rcc compiler for python for qt5') + + find_bin(['pylupdate5', 'pyside2-lupdate'], 'QT_PYLUPDATE') + find_bin(['lrelease-qt5', 'lrelease'], 'QT_LRELEASE') + + +def options(opt): + """ + Command-line options + """ + pass + From ad356b3ed226a14d9160cd68f99f3f7e400c10f8 Mon Sep 17 00:00:00 2001 From: fedepell Date: Sat, 23 Jul 2016 14:13:40 +0200 Subject: [PATCH 2/7] - Fixed identations - Search for tools just in PATH not in other directories as for C++ - Remove options handling as there is none at the moment - Use find_program instead of local find_bin - Fix author - Try to make documentation clearer - Remove useless after_link decorator --- waflib/extras/pyqt5.py | 92 ++++++++---------------------------------- 1 file changed, 17 insertions(+), 75 deletions(-) diff --git a/waflib/extras/pyqt5.py b/waflib/extras/pyqt5.py index d82048f4..6016768a 100644 --- a/waflib/extras/pyqt5.py +++ b/waflib/extras/pyqt5.py @@ -1,6 +1,5 @@ #!/usr/bin/env python # encoding: utf-8 -# Thomas Nagy, 2006-2016 (ita) original C++ QT5 implementation # Federico Pellegrin, 2016 (fedepell) adapted for Python """ @@ -29,10 +28,10 @@ Usage Load the "pyqt5" tool. -Reference the qrc resources files or ui5 definition files as -your sources and they will be translated into python code with -the system tools (PyQt5 or pyside2 are searched in this order) -and then compiled +Add into the sources list also the qrc resources files or ui5 +definition files as your sources and they will be translated +into python code with the system tools (PyQt5 or pyside2 are +searched in this order) and then compiled """ try: @@ -47,7 +46,7 @@ else: import os, sys from waflib.Tools import python from waflib import Task, Utils, Options, Errors, Context -from waflib.TaskGen import feature, after_method, extension, before_method +from waflib.TaskGen import feature, extension from waflib.Configure import conf from waflib import Logs @@ -81,16 +80,16 @@ class XMLHandler(ContentHandler): @extension(*EXT_RCC) def create_pyrcc_task(self, node): "Creates rcc and py task for ``.qrc`` files" - rcnode = node.change_ext('.py') - self.create_task('pyrcc', node, rcnode) - self.process_py(rcnode) + rcnode = node.change_ext('.py') + self.create_task('pyrcc', node, rcnode) + self.process_py(rcnode) @extension(*EXT_UI) def create_pyuic_task(self, node): "Create uic tasks and py for user interface ``.ui`` definition files" - uinode = node.change_ext('.py') - self.create_task('ui5py', node, uinode) - self.process_py(uinode) + uinode = node.change_ext('.py') + self.create_task('ui5py', node, uinode) + self.process_py(uinode) @extension('.ts') def add_pylang(self, node): @@ -98,7 +97,6 @@ def add_pylang(self, node): self.lang = self.to_list(getattr(self, 'lang', [])) + [node] @feature('pyqt5') -@after_method('apply_link') def apply_pyqt5(self): """ The additional parameters are: @@ -133,7 +131,6 @@ class pyrcc(Task.Task): run_str = '${QT_PYRCC} ${SRC} -o ${TGT}' ext_out = ['.py'] - def rcname(self): return os.path.splitext(self.inputs[0].name)[0] @@ -197,8 +194,8 @@ def configure(self): The detection uses the program ``pkg-config`` through :py:func:`waflib.Tools.config_c.check_cfg` """ - self.find_pyqt5_binaries() - + self.find_pyqt5_binaries() + # warn about this during the configuration too if not has_xml: Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!') @@ -211,70 +208,15 @@ def find_pyqt5_binaries(self): Detects Qt programs such as qmake, moc, uic, lrelease """ env = self.env - opt = Options.options - qtdir = getattr(opt, 'qtdir', '') - qtbin = getattr(opt, 'qtbin', '') - - paths = [] - - if qtdir: - qtbin = os.path.join(qtdir, 'bin') - - # the qt directory has been given from QT5_ROOT - deduce the qt binary path - if not qtdir: - qtdir = self.environ.get('QT5_ROOT', '') - qtbin = self.environ.get('QT5_BIN') or os.path.join(qtdir, 'bin') - - if qtbin: - paths = [qtbin] - - # no qtdir, look in the path and in /usr/local/Trolltech - if not qtdir: - paths = self.environ.get('PATH', '').split(os.pathsep) - paths.extend(['/usr/share/qt5/bin', '/usr/local/lib/qt5/bin']) - try: - lst = Utils.listdir('/usr/local/Trolltech/') - except OSError: - pass - else: - if lst: - lst.sort() - lst.reverse() - - # keep the highest version - qtdir = '/usr/local/Trolltech/%s/' % lst[0] - qtbin = os.path.join(qtdir, 'bin') - paths.append(qtbin) - - - def find_bin(lst, var): - if var in env: - return - for f in lst: - try: - ret = self.find_program(f, path_list=paths) - except self.errors.ConfigurationError: - pass - else: - env[var]=ret - break - - find_bin(['pyuic5','pyside2-uic'], 'QT_PYUIC') + self.find_program(['pyuic5','pyside2-uic'], var='QT_PYUIC') if not env.QT_PYUIC: self.fatal('cannot find the uic compiler for python for qt5') - find_bin(['pyrcc5','pyside2-rcc'], 'QT_PYRCC') + self.find_program(['pyrcc5','pyside2-rcc'], var='QT_PYRCC') if not env.QT_PYUIC: self.fatal('cannot find the rcc compiler for python for qt5') - find_bin(['pylupdate5', 'pyside2-lupdate'], 'QT_PYLUPDATE') - find_bin(['lrelease-qt5', 'lrelease'], 'QT_LRELEASE') - - -def options(opt): - """ - Command-line options - """ - pass + self.find_program(['pylupdate5', 'pyside2-lupdate'], var='QT_PYLUPDATE') + self.find_program(['lrelease-qt5', 'lrelease'], var='QT_LRELEASE') From 5d8c8a208025d967d0a95538355e4f14a4bd2819 Mon Sep 17 00:00:00 2001 From: fedepell Date: Sat, 23 Jul 2016 14:22:26 +0200 Subject: [PATCH 3/7] Fixes on comments --- waflib/extras/pyqt5.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/waflib/extras/pyqt5.py b/waflib/extras/pyqt5.py index 6016768a..00ac9209 100644 --- a/waflib/extras/pyqt5.py +++ b/waflib/extras/pyqt5.py @@ -187,13 +187,6 @@ class qm2rcc(Task.Task): self.outputs[0].write(code) def configure(self): - """ - Besides the configuration options, the environment variable QT5_ROOT may be used - to give the location of the qt5 libraries (absolute path). - - The detection uses the program ``pkg-config`` through :py:func:`waflib.Tools.config_c.check_cfg` - """ - self.find_pyqt5_binaries() # warn about this during the configuration too @@ -205,7 +198,7 @@ def configure(self): @conf def find_pyqt5_binaries(self): """ - Detects Qt programs such as qmake, moc, uic, lrelease + Detects PyQt5 or pyside2 programs such as pyuic5/pyside2-uic, pyrcc5/pyside2-rcc """ env = self.env From d3367e9b0a03cc78a8162c51f5de4aa30db9e73d Mon Sep 17 00:00:00 2001 From: fedepell Date: Sat, 23 Jul 2016 14:24:57 +0200 Subject: [PATCH 4/7] Clear up usage text --- waflib/extras/pyqt5.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/waflib/extras/pyqt5.py b/waflib/extras/pyqt5.py index 00ac9209..307a973b 100644 --- a/waflib/extras/pyqt5.py +++ b/waflib/extras/pyqt5.py @@ -29,9 +29,9 @@ Usage Load the "pyqt5" tool. Add into the sources list also the qrc resources files or ui5 -definition files as your sources and they will be translated -into python code with the system tools (PyQt5 or pyside2 are -searched in this order) and then compiled +definition files and they will be translated into python code +with the system tools (PyQt5 or pyside2 are searched in this +order) and then compiled """ try: From 7ade9796a6e3a1ad9f23264012cd99005494a033 Mon Sep 17 00:00:00 2001 From: fedepell Date: Thu, 28 Jul 2016 18:13:26 +0200 Subject: [PATCH 5/7] Fix installation path for .py files that are generated on the fly in build --- waflib/extras/pyqt5.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/waflib/extras/pyqt5.py b/waflib/extras/pyqt5.py index 307a973b..a3f5537b 100644 --- a/waflib/extras/pyqt5.py +++ b/waflib/extras/pyqt5.py @@ -82,6 +82,12 @@ def create_pyrcc_task(self, node): "Creates rcc and py task for ``.qrc`` files" rcnode = node.change_ext('.py') self.create_task('pyrcc', node, rcnode) + if self.install_from: + pyd = Context.out_dir[len(Context.top_dir)+1:] + str(self.install_from)[len(Context.top_dir):] + else: + pyd = Context.out_dir[len(Context.top_dir)+1:] + + self.install_from = self.path.find_dir(pyd) self.process_py(rcnode) @extension(*EXT_UI) @@ -89,6 +95,12 @@ def create_pyuic_task(self, node): "Create uic tasks and py for user interface ``.ui`` definition files" uinode = node.change_ext('.py') self.create_task('ui5py', node, uinode) + if self.install_from: + pyd = Context.out_dir[len(Context.top_dir) + 1:] + str(self.install_from)[len(Context.top_dir):] + else: + pyd = Context.out_dir[len(Context.top_dir) + 1:] + + self.install_from = self.path.find_dir(pyd) self.process_py(uinode) @extension('.ts') From f4e1b59bbce1286ab6ca0349dca6ff8d0dd53fb6 Mon Sep 17 00:00:00 2001 From: fedepell Date: Fri, 29 Jul 2016 16:36:01 +0200 Subject: [PATCH 6/7] Fix install_from after suggestion of ita on how to do it better and make sure install_path is present as python requires it --- waflib/extras/pyqt5.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/waflib/extras/pyqt5.py b/waflib/extras/pyqt5.py index a3f5537b..77dba3c6 100644 --- a/waflib/extras/pyqt5.py +++ b/waflib/extras/pyqt5.py @@ -82,12 +82,11 @@ def create_pyrcc_task(self, node): "Creates rcc and py task for ``.qrc`` files" rcnode = node.change_ext('.py') self.create_task('pyrcc', node, rcnode) - if self.install_from: - pyd = Context.out_dir[len(Context.top_dir)+1:] + str(self.install_from)[len(Context.top_dir):] + if getattr(self, 'install_from', None): + self.install_from = self.install_from.get_bld() else: - pyd = Context.out_dir[len(Context.top_dir)+1:] - - self.install_from = self.path.find_dir(pyd) + self.install_from = self.path.get_bld() + self.install_path = getattr(self, 'install_path', '${PYTHONDIR}') self.process_py(rcnode) @extension(*EXT_UI) @@ -95,12 +94,11 @@ def create_pyuic_task(self, node): "Create uic tasks and py for user interface ``.ui`` definition files" uinode = node.change_ext('.py') self.create_task('ui5py', node, uinode) - if self.install_from: - pyd = Context.out_dir[len(Context.top_dir) + 1:] + str(self.install_from)[len(Context.top_dir):] + if getattr(self, 'install_from', None): + self.install_from = self.install_from.get_bld() else: - pyd = Context.out_dir[len(Context.top_dir) + 1:] - - self.install_from = self.path.find_dir(pyd) + self.install_from = self.path.get_bld() + self.install_path = getattr(self, 'install_path', '${PYTHONDIR}') self.process_py(uinode) @extension('.ts') From 47ac970d159e5d41c1751d08faf874cdae77bc8f Mon Sep 17 00:00:00 2001 From: fedepell Date: Fri, 29 Jul 2016 16:37:58 +0200 Subject: [PATCH 7/7] Added pyqt5 playground example --- playground/pyqt5/res/test.txt | 2 + playground/pyqt5/sampleRes.qrc | 5 ++ playground/pyqt5/src/firstgui.ui | 130 +++++++++++++++++++++++++++++++ playground/pyqt5/src/sample.py | 24 ++++++ playground/pyqt5/wscript | 29 +++++++ 5 files changed, 190 insertions(+) create mode 100644 playground/pyqt5/res/test.txt create mode 100644 playground/pyqt5/sampleRes.qrc create mode 100644 playground/pyqt5/src/firstgui.ui create mode 100644 playground/pyqt5/src/sample.py create mode 100644 playground/pyqt5/wscript diff --git a/playground/pyqt5/res/test.txt b/playground/pyqt5/res/test.txt new file mode 100644 index 00000000..9b227d91 --- /dev/null +++ b/playground/pyqt5/res/test.txt @@ -0,0 +1,2 @@ +change me to see qrc dependencies! + diff --git a/playground/pyqt5/sampleRes.qrc b/playground/pyqt5/sampleRes.qrc new file mode 100644 index 00000000..687c5100 --- /dev/null +++ b/playground/pyqt5/sampleRes.qrc @@ -0,0 +1,5 @@ + + + res/test.txt + + diff --git a/playground/pyqt5/src/firstgui.ui b/playground/pyqt5/src/firstgui.ui new file mode 100644 index 00000000..cb7f9d30 --- /dev/null +++ b/playground/pyqt5/src/firstgui.ui @@ -0,0 +1,130 @@ + + + myfirstgui + + + + 0 + 0 + 411 + 247 + + + + My First Gui! + + + + + 20 + 210 + 381 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + 10 + 10 + 101 + 21 + + + + + + + 120 + 10 + 281 + 192 + + + + + + + 10 + 180 + 101 + 23 + + + + clear + + + + + + 10 + 40 + 101 + 23 + + + + add + + + + + + + buttonBox + accepted() + myfirstgui + accept() + + + 258 + 274 + + + 157 + 274 + + + + + buttonBox + rejected() + myfirstgui + reject() + + + 316 + 260 + + + 286 + 274 + + + + + clearBtn + clicked() + listWidget + clear() + + + 177 + 253 + + + 177 + 174 + + + + + diff --git a/playground/pyqt5/src/sample.py b/playground/pyqt5/src/sample.py new file mode 100644 index 00000000..80335e7c --- /dev/null +++ b/playground/pyqt5/src/sample.py @@ -0,0 +1,24 @@ +import sys +from PyQt5 import QtCore, QtGui, QtWidgets +from firstgui import Ui_myfirstgui + +class MyFirstGuiProgram(Ui_myfirstgui): + def __init__(self, dialog): + Ui_myfirstgui.__init__(self) + self.setupUi(dialog) + + # Connect "add" button with a custom function (addInputTextToListbox) + self.addBtn.clicked.connect(self.addInputTextToListbox) + + def addInputTextToListbox(self): + txt = self.myTextInput.text() + self.listWidget.addItem(txt) + +if __name__ == '__main__': + app = QtWidgets.QApplication(sys.argv) + dialog = QtWidgets.QDialog() + + prog = MyFirstGuiProgram(dialog) + + dialog.show() + sys.exit(app.exec_()) diff --git a/playground/pyqt5/wscript b/playground/pyqt5/wscript new file mode 100644 index 00000000..8a5379be --- /dev/null +++ b/playground/pyqt5/wscript @@ -0,0 +1,29 @@ +#! /usr/bin/env python +# encoding: utf-8# +# Federico Pellegrin, 2016 (fedepell) + +""" +Python QT5 helper tools example: +converts QT5 Designer tools files (UI and QRC) into python files with +the appropriate tools (pyqt5 and pyside2 searched) and manages their +python compilation and installation using standard python waf Tool + +""" +def options(opt): + # Load also python to demonstrate mixed calls + opt.load('python pyqt5') + +def configure(conf): + # Load also python to demonstrate mixed calls + conf.load('python pyqt5') + conf.check_python_version((2,7,4)) + +def build(bld): + # Demostrates mixed usage of py and pyqt5 module, and tests also install_path and install_from + # (since generated files go into build it has to be reset inside the pyqt5 tool) + bld(features="py pyqt5", source="src/sample.py src/firstgui.ui", install_path="${PREFIX}/play/", install_from="src/") + + # Simple usage on a resource file. If a file referenced inside the resource changes it will be rebuilt + # as the qrc XML is parsed and dependencies are calculated + bld(features="pyqt5", source="sampleRes.qrc") +