Esempio n. 1
0
def static_qml_basic_types(file_i, file_o):
    """ 统计 qml 的基本类型有哪些.
    
    Args:
        file_i: 'blueprint/resources/no5_all_qml_widgets.json'
            structure: {
                module: {
                    qmltype: {
                        'parent': ...,
                        'props': {prop: type, ...}
                    }, ...              ^--^ 我们统计的是这个.
                }, ...
            }
        file_o: *.txt or empty str. the empty string means 'donot dump to file,
            just print it on the console'.
            structure: [type, ...]. 一个去重后的列表, 按照字母表顺序排列.
            
    Outputs:
        data_w: {type: [(module, qmltype, prop), ...], ...}
    """
    data_r = loads(file_i)
    data_w = defaultdict(set)  # type: dict[str, set[tuple[str, str, str]]]

    for k1, v1 in data_r.items():
        for k2, v2 in v1.items():
            for k3, v3 in v2['props'].items():
                # k1: module; k2: qmltype; k3: prop; v3: type
                data_w[v3].add((k1, k2, k3))

    [print(i, k) for i, k in enumerate(sorted(data_w.keys()), 1)]
    if file_o:
        data_w = {k: sorted(data_w[k]) for k in sorted(data_w.keys())}
        dumps(data_w, file_o, pretty_dump=True)
Esempio n. 2
0
    def __init__(self, python_interpreter=''):
        """
        
        Args:
            python_interpreter: 如果启用了虚拟环境, 则这里传入 `.venv_builder:
                VEnvBuilder:get_embed_python:returns`; 如果没使用虚拟环境, 则留
                空, 它将使用默认 (全局的) python 解释器和 pyarmor. 此时请确保该
                解释器和 pyarmor 都可以在 cmd 中访问 (测试命令: `python
                --version`, `pyarmor --version`).
                参考: https://pyarmor.readthedocs.io/zh/latest/advanced.html
                    子章节: "使用不同版本 Python 加密脚本"
        
        Warnings:
            Currently only supports Windows platform.
        """
        self._liscense = 'trial'
        #   TODO: see https://pyarmor.readthedocs.io/zh/latest/license.html

        if python_interpreter:
            # warnings: `docs/devnote/warnings-about-embed-python.md`
            self._interpreter = python_interpreter.replace('/', '\\')
            # create pyarmor file copy
            origin_file = self._locate_pyarmor_script()
            backup_file = origin_file.removesuffix('.py') + '_copy.py'
            if not ospath.exists(backup_file):
                from lk_utils.read_and_write import loads, dumps
                content = loads(origin_file)
                dumps(
                    '\n'.join([
                        'from os.path import dirname',
                        'from sys import path as syspath',
                        'syspath.append(dirname(__file__))', content
                    ]), backup_file)
            self._pyarmor = backup_file
            self._head = f'"{self._interpreter}" "{self._pyarmor}"'
        else:
            self._interpreter = 'python'
            self._pyarmor = 'pyarmor'
            self._head = 'pyarmor'
Esempio n. 3
0
def _create_launcher(app_name,
                     icon,
                     target,
                     root_dir,
                     pyversion,
                     extend_sys_paths=None,
                     enable_venv=True,
                     enable_console=True,
                     create_launch_bat=True):
    """ Create launcher ({srcdir}/bootloader.py).

    Args:
        app_name (str): application name, this will be used as exe file's name:
            e.g. `app_name = 'Hello World'` -> will generate 'Hello World.exe'
        icon (str): *.ico file
        target (dict): {
            'file': filepath,
            'function': str,
            'args': [...],
            'kwargs': {...}
        }
        root_dir (str):
        pyversion (str):
        extend_sys_paths (list[str]):
        enable_venv (bool):
        enable_console (bool):
        create_launch_bat (bool):
    
    详细说明
        启动器分为两部分, 一个是启动器图标, 一个引导装载程序.
        启动器图标位于: '{root_dir}/{app_name}.exe'
        引导装载程序位于: '{root_dir}/src/bootloader.pyc'
    
        1. 二者的体积都非常小
        2. 启动器本质上是一个带有自定义图标的 bat 脚本. 它指定了 Python 编译器的
           路径和 bootloader 的路径, 通过调用编译器执行 bootloader.pyc
        3. bootloader 主要完成了以下两项工作:
            1. 向 sys.path 中添加当前工作目录和自定义的模块目录
            2. 对主脚本加了一个 try catch 结构, 以便于捕捉主程序报错时的信息, 并
               以系统弹窗的形式给用户. 这样就摆脱了控制台打印的需要, 使我们的软
               件表现得更像是一款软件

    Notes:
        1. 启动器在调用主脚本 (`func:main::args:main_script`) 之前, 会通过
           `os.chdir` 切换到主脚本所在的目录, 这样我们项目源代码中的所有相对路
           径, 相对引用都能正常工作

    References:
        - template/launch_by_system.bat
        - template/launch_by_venv.bat
        - template/bootloader.txt
    
    Returns:
        launch_file: ``{root_dir}/src/{bootloader_name}.py``.

    """
    launcher_name = app_name
    bootloader_name = 'bootloader'

    target_path = target['file']  # type: str
    target_dir = global_dirs.to_dist(ospath.dirname(target_path))
    launch_dir = ospath.dirname(target_dir)
    #   the launcher dir is parent of target dir, i.e. we put the launcher file
    #   in the parent folder of target file's folder.

    # target_reldir: 'target relative directory' (starts from `launch_dir`)
    # PS: it is equivalent to f'{target_dir_name}/{target_file_name}'
    target_reldir = global_dirs.relpath(target_dir, launch_dir)
    target_pkg = target_reldir.replace('/', '.')
    target_name = filesniff.get_filename(target_path, suffix=False)

    extend_sys_paths = list(
        map(
            lambda d: global_dirs.relpath(
                global_dirs.to_dist(d)
                if not d.startswith('dist:') else d.replace(
                    'dist:root', f'{root_dir}'), launch_dir),
            extend_sys_paths))

    template = loads(global_dirs.template('bootloader.txt'))
    code = template.format(
        # see `template/bootloader.txt > docstring:placeholders`
        LIB_RELDIR=global_dirs.relpath(f'{root_dir}/lib', launch_dir),
        SITE_PACKAGES='../venv/site-packages' if enable_venv else '',
        EXTEND_SYS_PATHS=str(extend_sys_paths),
        TARGET_RELDIR=target_reldir,
        TARGET_PKG=target_pkg,
        TARGET_NAME=target_name,
        TARGET_FUNC=target['function'] or '_',
        #   注意这里的 `target['function']`, 它有以下几种情况:
        #       target['function'] = some_str
        #           对应于 `../template/bootloader.txt(后面简称 'bootloader')
        #           ::底部位置::cmt:CASE3`
        #       target['function'] = '*'
        #           对应于 `bootloader::底部位置::cmt:CASE2`
        #       target['function'] = ''
        #           对应于 `bootloader::底部位置::cmt:CASE1`
        #   对于 CASE 1, 也就是 `target['function']` 为空字符串的情况, 我们必须
        #   将其改为其他字符 (这里就用了下划线作替代). 否则, 会导致打包后的
        #   `bootloader.py::底部位置::cmt:CASE3` 语句无法通过 Python 解释器.
        TARGET_ARGS=str(target['args']),
        TARGET_KWARGS=str(target['kwargs']),
    )
    dumps(code, launch_file := f'{launch_dir}/{bootloader_name}.py')

    # --------------------------------------------------------------------------

    # template = loads(global_dirs.template('pytransform.txt'))
    # code = template.format(
    #     LIB_PARENT_RELDIR='../'
    # )
    # dumps(code, f'{root_dir}/src/pytransform.py')

    # --------------------------------------------------------------------------

    if create_launch_bat is False:
        return launch_file

    if enable_venv:  # suggest
        template = loads(global_dirs.template('launch_by_venv.bat'))
    else:
        template = loads(global_dirs.template('launch_by_system.bat'))
    code = template.format(
        PYVERSION=pyversion.replace('.', ''),  # ...|'37'|'38'|'39'|...
        VENV_RELDIR=global_dirs.relpath(f'{root_dir}/venv',
                                        launch_dir).replace('/', '\\'),
        LAUNCHER_RELDIR=global_dirs.relpath(launch_dir,
                                            root_dir).replace('/', '\\'),
        LAUNCHER_NAME=f'{bootloader_name}.py',
    )
    bat_file = f'{root_dir}/{launcher_name}.bat'
    # lk.logt('[D3432]', code)
    dumps(code, bat_file)

    # 这是一个耗时操作 (大约需要 10s), 我们把它放在子线程执行
    def generate_exe(bat_file, exe_file, icon_file, *options):
        from .bat_2_exe import bat_2_exe
        lk.loga('converting bat to exe... '
                'it may take several seconds ~ one minute...')
        bat_2_exe(bat_file, exe_file, icon_file, *options)
        lk.loga('convertion bat-to-exe done')

    global thread
    thread = runnin_new_thread(generate_exe, bat_file,
                               f'{root_dir}/{launcher_name}.exe', icon, '/x64',
                               '' if enable_console else '/invisible')

    return launch_file
                with VBox() as vbox:
                    with Label() as label:
                        label.setText('Hello World')
                        vbox.addWidget(label)
    """
    from PySide6 import QtWidgets

    from .base_component import BaseComponent


    {HTML_COMPONENTS}
''')

template = _fmt_string('''
    class {WIDGET_NAME}(QtWidgets.Q{WIDGET_NAME}, BaseComponent):
        pass
''')

components = []

for tag in sorted(load_list('test.txt')):
    if tag == '': continue
    components.append(
        template.format(WIDGET_NAME=tag)
    )

dumps(
    file_template.format(HTML_COMPONENTS='\n\n'.join(components)).rstrip(),
    '../components/qt_widgets.py'
)
Esempio n. 5
0
def main(file_i: str, file_o):
    """
    Args:
        file_i: "~/blueprint/resources/no1_all_qml_modules.html". 该文件被我事先
            从 "{YourQtProgram}/Docs/Qt-{version}/qtdoc/modules-qml.html" 拷贝过
            来.
        file_o: '~/blueprint/resources/no2_all_qml_modules.json'
            "~/resources/all_qml_modules.json"
                格式: {
                    'module_group': {raw_module_group: formatted_name, ...},
                        raw_module_group: see `Notes:no1`
                        formatted_name: see `Notes:no3`
                    'module': {raw_module: formatted_name, ...}
                        raw_module: see `Notes:no2`
                }
                示例: {
                    'module_group': {
                        'qtquick': 'QtQuick',
                        'qtquickcontrols': 'QtQuickControls',
                        ...
                    },
                    'module': {
                        'qtquick-windows': 'QtQuick.Windows',
                        ...
                    },
                }
            Notes:
                1. `raw_module_group` 的键是没有空格或连接符的, 只有纯小写字母和
                    数字组成
                2. `raw_module` 的键是由纯小写字母和连接符组成 (例如 'qtquick
                    -windows')
                3. `formatted_name` 是由首字母大写的单词和点号组成 (例如
                    'QtQuick.Windows')
                    1. 但是有一个特例: 'QtQuick.labs.xxx' 从 'lab' 开始全部都是
                        小写(例如 'Qt.labs.folderlistmodel')
                4. 该生成文件可被直接用于 `no2_all_qml_types.py.py:_correct
                    _module_lettercase`
    """
    file_i = file_i.replace('\\', '/')
    soup = BeautifulSoup(read_and_write.read_file(file_i), 'html.parser')
    container = soup.find('table', 'annotated')

    writer = {
        'module_group': {},  # value: {raw_module_group: formatted_name, ...}
        'module': {},  # value: {raw_module: formatted_name, ...}
    }

    extra_words = ['Qt', 'Quick', 'Qml', 'Win', 'Labs']

    for e in container.find_all('td', 'tblName'):
        """ <td class="tblName">
                <p>
                    <a href="../qtcharts/qtcharts-qmlmodule.html">
                                ^--1---^ ^--2---^
                        Qt Charts QML Types
                        ^---3---^
                    </a>
                </p>
            </td>
            
            -> 1. module_group: 'qtcharts'
               2. module: 'qtcharts'
               3. name: 'Qt Charts'
        """
        link = e.a['href'].split('/')
        # -> ['..', 'qtquickcontrols1', 'qtquick-controls-qmlmodule.html']

        module_group_raw = link[1]  # type: str
        # -> 'qtquickcontrols1'
        module_raw = link[2].replace('-qmlmodule.html', '')  # type: str
        # -> 'qtquick-controls'
        """ 针对 QtQuick Controls 的处理
        
        背景: Qt 对 QtQuick.Controls 的命名关系有点乱, 如下所示:
            QtQuick.Controls v1:
                module_group = 'qtquickcontrols1'
                module = 'qtquick-controls'
            QtQuick.Controls v2:
                module_group = 'qtquickcontrols'
                module = 'qtquick-controls2'
        我将 v1 舍弃, 只处理 v2, 并将 v2 的命名改为:
                module_group = 'qtquickcontrols'
                module = 'qtquick-controls' (注意去掉了尾部的数字 2)
        
        为什么这样做:
            以 Button 为例, v1 的 Button 继承于 FocusScope, v2 的 Button 继承于
            AbstractButton. 我的设计的前提是只使用 'qtquickcontrols' 和
            'qtquick-controls', 那么在这种情况下, 二者就只能保留其中一个模组. 因
            此我保留了 v2, 后续解析和分析继承关系也都基于 v2 继续.
        """
        if module_group_raw == 'qtquickcontrols1':
            continue
        if module_raw == 'qtquick-controls2':
            module_raw = 'qtquick-controls'

        mini_lexicon = (e.a.text.replace(' QML Types', '').replace(
            'Qt Quick',
            'QtQuick').replace('Qt3DAnimation',
                               'Animation').replace('Web', 'Web ').title()
                        )  # type: str
        """ 解释一下上面的 mini_lexicon 的处理逻辑.
            
            mini_lexicon 为 module_group 和 module 提供一个小型词典, 该词典可用
            于帮助调整 module_group 和 module 的大小写格式.
            例如:
                调整前:
                    module_group: 'qtcharts'
                    module: 'qtcharts'
                调整后:
                    module_group: 'QtCharts'
                    module: 'QtCharts'
            mini_lexicon 来源于 `e.a.text`, 在考虑到实际情况中, 有许多细节需要重
            新调整, 所以我们才要对 mini_lexicon 进行诸多处理, 才能为
            module_group 和 module 所用:
                1. `replace(' QML Types', '')`: 把不必要的词尾去掉
                2. `replace('Qt Quick', 'QtQuick')`: 遵循模块的写法规范
                3. `replace('Qt3DAnimation', 'Animation')`: 针对 'Qt 3D
                    Qt3DAnimation' 的处理. 这个貌似是官方的写法有点问题, 所以我
                    把 'Qt3DAnimation' 改成了 'Animation'
                4. `replace('Web', 'Web ')`: 为了将 'WebEngine' 拆分成
                    'Web Engine', 需要 `mini_lexicon` 提供这两个独立的单词
                5. `title()`: 将首字母大写, 非首字母小写. 例如:
                    1. 'Qt NFS' -> 'Qt Nfc'
                    2. 'Qt QML' -> 'Qt Qml'
                    
            此外还有一些其他问题:
                1. module_group = 'qtwinextras' 的 `e.a.text` 是
                    'Qt Windows Extras', 该问题不属于 mini_lexicon 的处理范畴.
                    我使用 `extra_words` 变量解决这个问题, 见 extra_words 的定义
        """

        words = [x.title() for x in mini_lexicon.split(' ') if len(x) > 1]
        # -> ['QtQuick', 'Controls']
        module_group_fmt = _correct_module_lettercase(module_group_raw,
                                                      extra_words + words)
        module_fmt = _correct_module_lettercase(module_raw,
                                                extra_words + words)

        writer['module_group'][module_group_raw] = module_group_fmt
        writer['module'][module_raw] = module_fmt

    read_and_write.dumps(writer, file_o)
Esempio n. 6
0
def main(file_i, file_o):
    """
    
    Args:
        file_i: '~/blueprint/resources/no2_all_qml_types.html'. 该文件被我事先从
            "{YourQtProgram}/Docs/Qt-{version}/qtdoc/qmltypes.html" 拷贝过来.
        file_o: 生成文件. "~/blueprint/resources/no3_all_qml_types.json"
            {module_group: {module: {type_name: path}, ...}, ...}
            #   {模组: {模块: {类型: 路径}}}
            e.g. {
                'qtquick': {
                    'qtquick': {
                        'Rectangle': 'qtquick/qml-qtquick-rectangle.html',
                        'Text': 'qtquick/qml-qtquick-text.html',
                        ...
                    },
                    'qtquick-window': {
                        'Window': 'qtquick/qml-qtquick-window-window.html',
                        ...
                    },
                    ...
                },
                ...
            }
    
    思路:
        1. 我们安装了 Qt 主程序以后, 在软件安装目录下的 'Docs/Qt-{version}' 中有
           它的 API 文档
        2. 其中 "~/Docs/Qt-{version}/qtdoc/qmltypes.html" 列出了全部的 qml types
        3. 我们对 "qmltypes.html" 用 BeautifulSoup 解析, 从中获取每个 qml types
           和它的链接, 最终我们将得到这些信息: 模组, 模块, 类型, 路径等
        4. 将这些信息保存到本项目下的 "~/resources/qmltypes.json" 文件中
    """
    soup = BeautifulSoup(read_and_write.read_file(file_i), 'html.parser')

    # https://www.itranslater.com/qa/details/2325827141935563776
    data = defaultdict(lambda: defaultdict(dict))
    #   {module_group: {module: {type_name: filename, ...}, ...}, ...}

    container = soup.find('div', 'flowListDiv')
    for e in container.find_all('dd'):
        link = e.a['href']  # type: str
        #   e.g. "../qtdatavisualization/qml-qtdatavisualization-
        #         abstract3dseries.html"

        match = re.search(r'\.\./(\w+)/([-\w]+)\.html', link)
        #                  |     ^-1-^ ^--2---^      |
        #                  ^-------- group(0) -------^
        #   match.group(0): '../qtdatavisualization/qml-qtdatavisualization
        #       -abstract3dseries.html'
        #   match.group(1): 'qtdatavisualization'
        #   match.group(2): 'qml-qtdatavisualization-abstract3dseries'
        assert match, e

        module_group = match.group(1)
        module = match.group(2)
        # see `blueprint/qml_modules_indexing/no1_all_qml_modules.py:comments
        # :针对 QtQuick Controls 的处理`
        if module_group == 'qtquickcontrols1':
            continue
        if 'qtquick-controls2' in module:
            #   e.g. 'qml-qtquick-controls2-label'
            module = module.replace('controls2', 'controls')

        path = match.group(0).lstrip('../')
        #   -> 'qtdatavisualization/qml-qtdatavisualization-abstract3dseries
        #   .html'
        module_group = _correct_module_lettercase(module_group)
        #   'qtdatavisualization' -> 'QtDataVisualization'
        module = _correct_module_lettercase('-'.join(module.split('-')[1:-1]))
        #   eg1: 'qml-qtdatavisualization-abstract3dseries' -> ['qml',
        #   'qtdatavisualization', 'abstract3dseries'] -> [
        #   'qtdatavisualization'] -> 'qtdatavisualization'
        #   -> 'QtDataVisualization'
        #   eg2: 'qml-qt3d-input-abstractactioninput' -> ['qml', 'qt3d',
        #   'input', 'abstractactioninput'] -> ['qt3d', 'input',
        #   'abstractactioninput'] -> 'qt3d-input' -> 'Qt3D.Input'
        #   注: 为什么要舍去末尾的元素? 因为末尾的那个是 `type_name`, 不是
        #   `module`. 接下来我们会抽取 `type_name`.
        type_name = e.text.split(':', 1)[0]
        #   注意我们不使用 `correct_module_lettercase(match.group(2).split('-')
        #   [-1])`, 是因为 `correct_module_lettercase` 的词库范围比较小, 仅对
        #   `module_group` 和 `module` 做了覆盖, 不能保证对 `type_name` 的处理正
        #   确; 而 `soup` 是可以比较轻松地通过 tag 提取到它的, 所以通过 html 元
        #   素获取.
        #   e.g. 'RadioButton: QtQuickControls' -> 'RadioButton'

        lk.loga(module_group, module, type_name)
        data[module_group][module][type_name] = path

    read_and_write.dumps(data, file_o)