Browse Source

Bundle pyqt_distutils

HEAD
T-bone 6 years ago
parent
commit
3d69db0507

+ 8
- 0
akvo/thirdparty/pyqt_ditutils/__init__.py View File

@@ -0,0 +1,8 @@
1
+"""
2
+A set of PyQt distutils extensions for build qt ui files in a pythonic way:
3
+
4
+    - build_ui: build qt ui/qrc files
5
+
6
+"""
7
+
8
+__version__ = '0.7.3'

+ 117
- 0
akvo/thirdparty/pyqt_ditutils/build_ui.py View File

@@ -0,0 +1,117 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+Distutils extension for PyQt/PySide applications
4
+"""
5
+import glob
6
+import os
7
+import subprocess
8
+import sys
9
+
10
+from setuptools import Command
11
+
12
+from .config import Config
13
+from .hooks import load_hooks
14
+from .utils import build_args, write_message
15
+
16
+
17
+class build_ui(Command):
18
+    """
19
+    Builds the Qt ui files as described in pyuic.json (or pyuic.cfg).
20
+    """
21
+    user_options = [
22
+        ('force', None,
23
+         'Force flag, will force recompilation of every ui/qrc file'),
24
+    ]
25
+
26
+    def initialize_options(self):
27
+        self.force = False
28
+        self._hooks = load_hooks()
29
+
30
+    def finalize_options(self):
31
+        try:
32
+            self.cfg = Config()
33
+            self.cfg.load()
34
+        except (IOError, OSError):
35
+            write_message('cannot open pyuic.json (or pyuic.cfg)', 'red')
36
+            self.cfg = None
37
+
38
+    def is_outdated(self, src, dst, ui):
39
+        if src.endswith('.qrc') or self.force:
40
+            return True
41
+        outdated = (not os.path.exists(dst) or
42
+                    (os.path.getmtime(src) > os.path.getmtime(dst)))
43
+        if not outdated and not ui:
44
+            # for qrc files, we need to check each individual resources.
45
+            # If one of them is newer than the dst file, the qrc file must be
46
+            # considered as outdated
47
+            # file paths are relative to the qrc file path
48
+            qrc_dirname = os.path.dirname(src)
49
+            with open(src, 'r') as f:
50
+                lines = f.read().splitlines()
51
+                lines = [l for l in lines if '<file>' in l]
52
+            cwd = os.getcwd()
53
+            os.chdir(qrc_dirname)
54
+            for line in lines:
55
+                filename = line.replace('<file>', '').replace(
56
+                    '</file>', '').strip()
57
+                filename = os.path.abspath(filename)
58
+                if os.path.getmtime(filename) > os.path.getmtime(dst):
59
+                    outdated = True
60
+                    break
61
+            os.chdir(cwd)
62
+        return outdated
63
+
64
+    def run(self):
65
+        if not self.cfg:
66
+            return
67
+        for glob_exp, dest in self.cfg.files:
68
+            for src in glob.glob(glob_exp):
69
+                if not os.path.exists(src):
70
+                    write_message('skipping target %s, file not found' % src, 'yellow')
71
+                    continue
72
+                src = os.path.join(os.getcwd(), src)
73
+                dst = os.path.join(os.getcwd(), dest)
74
+                ui = True
75
+                if src.endswith('.ui'):
76
+                    ext = '_ui.py'
77
+                    cmd = self.cfg.uic_command()
78
+                elif src.endswith('.qrc'):
79
+                    ui = False
80
+                    ext = '_rc.py'
81
+                    cmd = self.cfg.rcc_command()
82
+                else:
83
+                    continue
84
+                filename = os.path.split(src)[1]
85
+                filename = os.path.splitext(filename)[0]
86
+                dst = os.path.join(dst, filename + ext)
87
+                try:
88
+                    os.makedirs(os.path.split(dst)[0])
89
+                except OSError:
90
+                    pass
91
+
92
+                if self.is_outdated(src, dst, ui):
93
+                    try:
94
+                        cmd = build_args(cmd, src, dst)
95
+                        subprocess.check_call(cmd)
96
+                        cmd = ' '.join(cmd)
97
+                    except subprocess.CalledProcessError as e:
98
+                        if e.output:
99
+                            write_message(cmd, 'yellow')
100
+                            write_message(e.output.decode(sys.stdout.encoding), 'red')
101
+                        else:
102
+                            write_message(cmd, 'red')
103
+                    except OSError as e:
104
+                        write_message(cmd, 'yellow')
105
+                        write_message(str(e), 'red')
106
+                    else:
107
+                        write_message(cmd, 'green')
108
+                    for hookname in self.cfg.hooks:
109
+                        try:
110
+                            hook = self._hooks[hookname]
111
+                        except KeyError:
112
+                            write_message('warning, unknonw hook: %r' % hookname, 'yellow')
113
+                        else:
114
+                            write_message('running hook %r' % hookname, 'blue')
115
+                            hook(dst)
116
+                else:
117
+                    write_message('skipping %s, up to date' % src)

+ 91
- 0
akvo/thirdparty/pyqt_ditutils/config.py View File

@@ -0,0 +1,91 @@
1
+"""
2
+Contains the config class (pyuic.cfg or pyuic.json)
3
+
4
+"""
5
+import json
6
+
7
+from .utils import write_message
8
+
9
+class QtApi:
10
+    pyqt4 = 0
11
+    pyqt5 = 1
12
+    pyside = 2
13
+
14
+
15
+class Config:
16
+    def __init__(self):
17
+        self.files = []
18
+        self.pyuic = ''
19
+        self.pyuic_options = ''
20
+        self.pyrcc = ''
21
+        self.pyrcc_options = ''
22
+        self.hooks = []
23
+
24
+    def uic_command(self):
25
+        return self.pyuic + ' ' + self.pyuic_options + ' %s -o %s'
26
+
27
+    def rcc_command(self):
28
+        return self.pyrcc + ' ' + self.pyrcc_options + ' %s -o %s'
29
+
30
+    def load(self):
31
+        for ext in ['.cfg', '.json']:
32
+            try:
33
+                with open('pyuic' + ext, 'r') as f:
34
+                    self.__dict__ = json.load(f)
35
+            except (IOError, OSError):
36
+                pass
37
+            else:
38
+                break
39
+        else:
40
+            write_message('failed to open configuration file', 'yellow')
41
+        if not hasattr(self, 'hooks'):
42
+            self.hooks = []
43
+
44
+    def save(self):
45
+        with open('pyuic.json', 'w') as f:
46
+            json.dump(self.__dict__, f, indent=4, sort_keys=True)
47
+
48
+    def generate(self, api):
49
+        if api == QtApi.pyqt4:
50
+            self.pyrcc = 'pyrcc4'
51
+            self.pyrcc_options = '-py3'
52
+            self.pyuic = 'pyuic4'
53
+            self.pyuic_options = '--from-import'
54
+            self.files[:] = []
55
+        elif api == QtApi.pyqt5:
56
+            self.pyrcc = 'pyrcc5'
57
+            self.pyrcc_options = ''
58
+            self.pyuic = 'pyuic5'
59
+            self.pyuic_options = '--from-import'
60
+            self.files[:] = []
61
+        elif api == QtApi.pyside:
62
+            self.pyrcc = 'pyside-rcc'
63
+            self.pyrcc_options = '-py3'
64
+            self.pyuic = 'pyside-uic'
65
+            self.pyuic_options = '--from-import'
66
+            self.files[:] = []
67
+        self.save()
68
+        write_message('pyuic.json generated', 'green')
69
+
70
+    def add(self, src, dst):
71
+        self.load()
72
+        for fn, _ in self.files:
73
+            if fn == src:
74
+                write_message('ui file already added: %s' % src)
75
+                return
76
+        self.files.append((src, dst))
77
+        self.save()
78
+        write_message('file added to pyuic.json: %s -> %s' % (src, dst), 'green')
79
+
80
+    def remove(self, filename):
81
+        self.load()
82
+        to_remove = None
83
+        for i, files in enumerate(self.files):
84
+            src, dest = files
85
+            if filename == src:
86
+                to_remove = i
87
+                break
88
+        if to_remove is not None:
89
+            self.files.pop(to_remove)
90
+        self.save()
91
+        write_message('file removed from pyuic.json: %s' % filename, 'green')

+ 54
- 0
akvo/thirdparty/pyqt_ditutils/hooks.py View File

@@ -0,0 +1,54 @@
1
+"""
2
+This module contains the hooks load and our builtin hooks.
3
+
4
+"""
5
+import re
6
+import pkg_resources
7
+
8
+from .utils import write_message
9
+
10
+
11
+#: Name of the entrypoint to use in setup.py
12
+ENTRYPOINT = 'pyqt_distutils_hooks'
13
+
14
+
15
+def load_hooks():
16
+    """
17
+    Load the exposed hooks.
18
+
19
+    Returns a dict of hooks where the keys are the name of the hook and the
20
+    values are the actual hook functions.
21
+    """
22
+    hooks = {}
23
+    for entrypoint in pkg_resources.iter_entry_points(ENTRYPOINT):
24
+        name = str(entrypoint).split('=')[0].strip()
25
+        try:
26
+            hook = entrypoint.load()
27
+        except Exception as e:
28
+            write_message('failed to load entry-point %r (error="%s")' % (name, e), 'yellow')
29
+        else:
30
+            hooks[name] = hook
31
+    return hooks
32
+
33
+
34
+def hook(ui_file_path):
35
+    """
36
+    This is the prototype of a hook function.
37
+    """
38
+    pass
39
+
40
+
41
+def gettext(ui_file_path):
42
+    """
43
+    Let you use gettext instead of the Qt tools for l18n
44
+    """
45
+    with open(ui_file_path, 'r') as fin:
46
+        content = fin.read()
47
+
48
+    # replace ``_translate("context", `` by ``_(``
49
+    content = re.sub(r'_translate\(".*",\s', '_(', content)
50
+    content = content.replace(
51
+        '        _translate = QtCore.QCoreApplication.translate', '')
52
+
53
+    with open(ui_file_path, 'w') as fout:
54
+        fout.write(content)

+ 58
- 0
akvo/thirdparty/pyqt_ditutils/pyuicfg.py View File

@@ -0,0 +1,58 @@
1
+"""Help you manage your pyuic.json file (pyqt-distutils)
2
+
3
+Usage:
4
+    pyuicfg -g
5
+    pyuicfg -g --pyqt5
6
+    pyuicfg -g --pyside
7
+    pyuicfg -a SOURCE_FILE DESTINATION_PACKAGE
8
+    pyuicfg -r SOURCE_FILE
9
+    pyuicfg (-h | --help)
10
+    pyuicfg --version
11
+
12
+Options:
13
+    -h, --help                            Show help
14
+    --version                             Show version
15
+    -g                                    Generate pyuic.json
16
+    -a SOURCE_FILE DESTINATION_PACKAGE    Add file to pyuic.json
17
+    -r SOURCE_FILE                        Remove file from pyuic.json
18
+    --pyqt5                               Generate a pyuic.json file for PyQt5 instead of PyQt4
19
+    --pyside                              Generate a pyuic.json file for PySide instead of PyQt4
20
+
21
+"""
22
+import os
23
+from docopt import docopt
24
+from pyqt_distutils import __version__
25
+from pyqt_distutils.config import Config, QtApi
26
+
27
+
28
+def qt_api_from_args(arguments):
29
+    if arguments['--pyqt5']:
30
+        return QtApi.pyqt5
31
+    elif arguments['--pyside']:
32
+        return QtApi.pyside
33
+    return QtApi.pyqt4
34
+
35
+
36
+def main():
37
+    arguments = docopt(__doc__, version=__version__)
38
+    generate = arguments['-g']
39
+    file_to_add = arguments['-a']
40
+    destination_package = arguments['DESTINATION_PACKAGE']
41
+    file_to_remove = arguments['-r']
42
+    api = qt_api_from_args(arguments)
43
+    cfg = Config()
44
+    if generate:
45
+        if os.path.exists('pyuic.json'):
46
+            choice = input('pyuic.json already exists. Do you want to replace '
47
+                           'it? (y/N) ').lower()
48
+            if choice != 'y':
49
+                return
50
+        cfg.generate(api)
51
+    elif file_to_add:
52
+        cfg.add(file_to_add, destination_package)
53
+    elif file_to_remove:
54
+        cfg.remove(file_to_remove)
55
+
56
+
57
+if __name__ == '__main__':
58
+    main()

+ 45
- 0
akvo/thirdparty/pyqt_ditutils/utils.py View File

@@ -0,0 +1,45 @@
1
+try:
2
+    import colorama
3
+except ImportError:
4
+    has_colorama = False
5
+else:
6
+    has_colorama = True
7
+
8
+import shlex
9
+try:
10
+    # Python 3
11
+    from shlex import quote
12
+except ImportError:
13
+    # Python 2
14
+    from pipes import quote
15
+
16
+
17
+def build_args(cmd, src, dst):
18
+    """
19
+        Build arguments list for passing to subprocess.call_check
20
+
21
+        :param cmd str: Command string to interpolate src and dst filepaths into.
22
+            Typically the output of `config.Config.uic_command` or `config.Config.rcc_command`.
23
+        :param src str: Source filepath.
24
+        :param dst str: Destination filepath.
25
+    """
26
+    cmd = cmd % (quote(src), quote(dst))
27
+    args = shlex.split(cmd)
28
+
29
+    return [arg for arg in args if arg]
30
+
31
+
32
+def write_message(text, color=None):
33
+    if has_colorama:
34
+        colors = {
35
+            'red': colorama.Fore.RED,
36
+            'green': colorama.Fore.GREEN,
37
+            'yellow': colorama.Fore.YELLOW,
38
+            'blue': colorama.Fore.BLUE
39
+        }
40
+        try:
41
+            print(colors[color] + text + colorama.Fore.RESET)
42
+        except KeyError:
43
+            print(text)
44
+    else:
45
+        print(text)

Loading…
Cancel
Save