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)
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'
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' )
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)
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)