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): from lk_utils.read_and_write import loads self.module_namespace = loads( # type: Hint.ModuleNameSpace '../../data/pyml_import_namespaces.json') # e.g. {'pyml.qtquick': {'Text': {'imp}}} self.comp_namespace = {} # type: Hint.CompNameSpace self.imports = [] # DEL
def main(pyproj_file: str) -> TConf: """ Args: pyproj_file: see template at `./template/pyproject.json` References: docs/pyproject-template.md docs/devnote/difference-between-roots.md > h2:pyproj_root """ pyproj_root = pretty_path(ospath.dirname(pyproj_file)) lk.loga(pyproj_root) conf = loads(pyproj_file) # type: TConf conf = reformat_paths(conf, PathFormatter(pyproj_root)) return conf
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
# 注意我们不使用 `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) # ------------------------------------------------------------------------------ qml_modules = read_and_write.loads('../resources/no2_all_qml_modules.json') qml_modules = qml_modules['module_group'] | qml_modules['module'] # type: dict qml_modules.update({ # 扩充 '': '', 'qtquick-controls-private': 'QtQuick.Controls.Private', 'mediaplayer-qml': 'QtMediaPlayer', # 注: 这个其实是不存在的, 只是为了不报错所以加上去 }) def _correct_module_lettercase(module: str): """ 修正模块的大小写. 示例: 'qtquick-window' -> 'QtQuick.Window' 'qtgraphicaleffects' -> 'QtGraphicalEffects