1#!/usr/bin/env python 2# encoding: utf-8 3# Federico Pellegrin, 2016-2019 (fedepell) adapted for Python 4 5""" 6This tool helps with finding Python Qt5 tools and libraries, 7and provides translation from QT5 files to Python code. 8 9The following snippet illustrates the tool usage:: 10 11 def options(opt): 12 opt.load('py pyqt5') 13 14 def configure(conf): 15 conf.load('py pyqt5') 16 17 def build(bld): 18 bld( 19 features = 'py pyqt5', 20 source = 'main.py textures.qrc aboutDialog.ui', 21 ) 22 23Here, the UI description and resource files will be processed 24to generate code. 25 26Usage 27===== 28 29Load the "pyqt5" tool. 30 31Add into the sources list also the qrc resources files or ui5 32definition files and they will be translated into python code 33with the system tools (PyQt5, PySide2, PyQt4 are searched in this 34order) and then compiled 35""" 36 37try: 38 from xml.sax import make_parser 39 from xml.sax.handler import ContentHandler 40except ImportError: 41 has_xml = False 42 ContentHandler = object 43else: 44 has_xml = True 45 46import os 47from waflib.Tools import python 48from waflib import Task, Options 49from waflib.TaskGen import feature, extension 50from waflib.Configure import conf 51from waflib import Logs 52 53EXT_RCC = ['.qrc'] 54""" 55File extension for the resource (.qrc) files 56""" 57 58EXT_UI = ['.ui'] 59""" 60File extension for the user interface (.ui) files 61""" 62 63 64class XMLHandler(ContentHandler): 65 """ 66 Parses ``.qrc`` files 67 """ 68 def __init__(self): 69 self.buf = [] 70 self.files = [] 71 def startElement(self, name, attrs): 72 if name == 'file': 73 self.buf = [] 74 def endElement(self, name): 75 if name == 'file': 76 self.files.append(str(''.join(self.buf))) 77 def characters(self, cars): 78 self.buf.append(cars) 79 80@extension(*EXT_RCC) 81def create_pyrcc_task(self, node): 82 "Creates rcc and py task for ``.qrc`` files" 83 rcnode = node.change_ext('.py') 84 self.create_task('pyrcc', node, rcnode) 85 if getattr(self, 'install_from', None): 86 self.install_from = self.install_from.get_bld() 87 else: 88 self.install_from = self.path.get_bld() 89 self.install_path = getattr(self, 'install_path', '${PYTHONDIR}') 90 self.process_py(rcnode) 91 92@extension(*EXT_UI) 93def create_pyuic_task(self, node): 94 "Create uic tasks and py for user interface ``.ui`` definition files" 95 uinode = node.change_ext('.py') 96 self.create_task('ui5py', node, uinode) 97 if getattr(self, 'install_from', None): 98 self.install_from = self.install_from.get_bld() 99 else: 100 self.install_from = self.path.get_bld() 101 self.install_path = getattr(self, 'install_path', '${PYTHONDIR}') 102 self.process_py(uinode) 103 104@extension('.ts') 105def add_pylang(self, node): 106 """Adds all the .ts file into ``self.lang``""" 107 self.lang = self.to_list(getattr(self, 'lang', [])) + [node] 108 109@feature('pyqt5') 110def apply_pyqt5(self): 111 """ 112 The additional parameters are: 113 114 :param lang: list of translation files (\\*.ts) to process 115 :type lang: list of :py:class:`waflib.Node.Node` or string without the .ts extension 116 :param langname: if given, transform the \\*.ts files into a .qrc files to include in the binary file 117 :type langname: :py:class:`waflib.Node.Node` or string without the .qrc extension 118 """ 119 if getattr(self, 'lang', None): 120 qmtasks = [] 121 for x in self.to_list(self.lang): 122 if isinstance(x, str): 123 x = self.path.find_resource(x + '.ts') 124 qmtasks.append(self.create_task('ts2qm', x, x.change_ext('.qm'))) 125 126 127 if getattr(self, 'langname', None): 128 qmnodes = [k.outputs[0] for k in qmtasks] 129 rcnode = self.langname 130 if isinstance(rcnode, str): 131 rcnode = self.path.find_or_declare(rcnode + '.qrc') 132 t = self.create_task('qm2rcc', qmnodes, rcnode) 133 create_pyrcc_task(self, t.outputs[0]) 134 135class pyrcc(Task.Task): 136 """ 137 Processes ``.qrc`` files 138 """ 139 color = 'BLUE' 140 run_str = '${QT_PYRCC} ${SRC} -o ${TGT}' 141 ext_out = ['.py'] 142 143 def rcname(self): 144 return os.path.splitext(self.inputs[0].name)[0] 145 146 def scan(self): 147 """Parse the *.qrc* files""" 148 if not has_xml: 149 Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!') 150 return ([], []) 151 152 parser = make_parser() 153 curHandler = XMLHandler() 154 parser.setContentHandler(curHandler) 155 fi = open(self.inputs[0].abspath(), 'r') 156 try: 157 parser.parse(fi) 158 finally: 159 fi.close() 160 161 nodes = [] 162 names = [] 163 root = self.inputs[0].parent 164 for x in curHandler.files: 165 nd = root.find_resource(x) 166 if nd: 167 nodes.append(nd) 168 else: 169 names.append(x) 170 return (nodes, names) 171 172 173class ui5py(Task.Task): 174 """ 175 Processes ``.ui`` files for python 176 """ 177 color = 'BLUE' 178 run_str = '${QT_PYUIC} ${SRC} -o ${TGT}' 179 ext_out = ['.py'] 180 181class ts2qm(Task.Task): 182 """ 183 Generates ``.qm`` files from ``.ts`` files 184 """ 185 color = 'BLUE' 186 run_str = '${QT_LRELEASE} ${QT_LRELEASE_FLAGS} ${SRC} -qm ${TGT}' 187 188class qm2rcc(Task.Task): 189 """ 190 Generates ``.qrc`` files from ``.qm`` files 191 """ 192 color = 'BLUE' 193 after = 'ts2qm' 194 def run(self): 195 """Create a qrc file including the inputs""" 196 txt = '\n'.join(['<file>%s</file>' % k.path_from(self.outputs[0].parent) for k in self.inputs]) 197 code = '<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n%s\n</qresource>\n</RCC>' % txt 198 self.outputs[0].write(code) 199 200def configure(self): 201 self.find_pyqt5_binaries() 202 203 # warn about this during the configuration too 204 if not has_xml: 205 Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!') 206 207@conf 208def find_pyqt5_binaries(self): 209 """ 210 Detects PyQt5 or PySide2 programs such as pyuic5/pyside2-uic, pyrcc5/pyside2-rcc 211 """ 212 env = self.env 213 214 if getattr(Options.options, 'want_pyqt5', True): 215 self.find_program(['pyuic5'], var='QT_PYUIC') 216 self.find_program(['pyrcc5'], var='QT_PYRCC') 217 self.find_program(['pylupdate5'], var='QT_PYLUPDATE') 218 elif getattr(Options.options, 'want_pyside2', True): 219 self.find_program(['pyside2-uic'], var='QT_PYUIC') 220 self.find_program(['pyside2-rcc'], var='QT_PYRCC') 221 self.find_program(['pyside2-lupdate'], var='QT_PYLUPDATE') 222 elif getattr(Options.options, 'want_pyqt4', True): 223 self.find_program(['pyuic4'], var='QT_PYUIC') 224 self.find_program(['pyrcc4'], var='QT_PYRCC') 225 self.find_program(['pylupdate4'], var='QT_PYLUPDATE') 226 else: 227 self.find_program(['pyuic5','pyside2-uic','pyuic4'], var='QT_PYUIC') 228 self.find_program(['pyrcc5','pyside2-rcc','pyrcc4'], var='QT_PYRCC') 229 self.find_program(['pylupdate5', 'pyside2-lupdate','pylupdate4'], var='QT_PYLUPDATE') 230 231 if not env.QT_PYUIC: 232 self.fatal('cannot find the uic compiler for python for qt5') 233 234 if not env.QT_PYRCC: 235 self.fatal('cannot find the rcc compiler for python for qt5') 236 237 self.find_program(['lrelease-qt5', 'lrelease'], var='QT_LRELEASE') 238 239def options(opt): 240 """ 241 Command-line options 242 """ 243 pyqt5opt=opt.add_option_group("Python QT5 Options") 244 pyqt5opt.add_option('--pyqt5-pyqt5', action='store_true', default=False, dest='want_pyqt5', help='use PyQt5 bindings as python QT5 bindings (default PyQt5 is searched first, PySide2 after, PyQt4 last)') 245 pyqt5opt.add_option('--pyqt5-pyside2', action='store_true', default=False, dest='want_pyside2', help='use PySide2 bindings as python QT5 bindings (default PyQt5 is searched first, PySide2 after, PyQt4 last)') 246 pyqt5opt.add_option('--pyqt5-pyqt4', action='store_true', default=False, dest='want_pyqt4', help='use PyQt4 bindings as python QT5 bindings (default PyQt5 is searched first, PySide2 after, PyQt4 last)') 247