diff --git a/libs/lib_not_dr/__init__.py b/libs/lib_not_dr/__init__.py new file mode 100644 index 0000000..c0958cc --- /dev/null +++ b/libs/lib_not_dr/__init__.py @@ -0,0 +1,7 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +__version__ = '0.1.7' diff --git a/libs/lib_not_dr/command/__init__.py b/libs/lib_not_dr/command/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/lib_not_dr/command/data.py b/libs/lib_not_dr/command/data.py new file mode 100644 index 0000000..bfb5b3e --- /dev/null +++ b/libs/lib_not_dr/command/data.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass, field +from typing import Set, List + + +class Parsed: + ... + + +@dataclass +class Option: + name: str + shortcuts: List[str] + optional: bool + types: Set[type] = field(default_factory=lambda: {str}) + + +@dataclass +class OptionGroup: + options: List[Option] + optional: bool = True + exclusive: bool = False + + +@dataclass +class Argument: + name: str + types: Set[type] = field(default_factory=lambda: {str}) + + +@dataclass +class Flag: + name: str + shortcuts: List[str] + + +@dataclass +class FlagGroup: + flags: List[Flag] + exclusive: bool = False + diff --git a/libs/lib_not_dr/command/descriptor.py b/libs/lib_not_dr/command/descriptor.py new file mode 100644 index 0000000..c94c2ff --- /dev/null +++ b/libs/lib_not_dr/command/descriptor.py @@ -0,0 +1,14 @@ +class CallBackDescriptor: + def __init__(self, name): + self.callback_name = name + + def __set__(self, instance, value): + assert getattr(instance, self.callback_name) is None, f"Attribute '{self.callback_name}' has been set." + instance.__dict__[self.callback_name] = value + + def __get__(self, instance, owner): + return ( + self + if instance is None + else instance.__dict__.get(self.callback_name) + ) \ No newline at end of file diff --git a/libs/lib_not_dr/command/exception.py b/libs/lib_not_dr/command/exception.py new file mode 100644 index 0000000..2f79c39 --- /dev/null +++ b/libs/lib_not_dr/command/exception.py @@ -0,0 +1,2 @@ +class IllegalName(Exception): + """名称或快捷名不合法""" diff --git a/libs/lib_not_dr/command/nodes.py b/libs/lib_not_dr/command/nodes.py new file mode 100644 index 0000000..2f1158f --- /dev/null +++ b/libs/lib_not_dr/command/nodes.py @@ -0,0 +1,130 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +import re +from typing import Callable, List, Optional, Union, Set + +from .data import Option, Argument, Flag, Parsed +from .descriptor import CallBackDescriptor + +try: + from typing import Self +except ImportError: + from typing import TypeVar + Self = TypeVar("Self") # NOQA + +from .exception import IllegalName + +CallBack = Union[Callable[[str], None], str] # Equals to `Callable[[str], None] | str` +# 可调用对象或字符串作为回调 +# A callable or str as callback + +ParseArgFunc = Callable[[str], Optional[type]] +# 解析参数的函数,返回值为 None 时表示解析失败 +# function to parse argument, return None when failed + +EMPTY_WORDS = re.compile(r"\s", re.I) + + +def check_name(name: Union[str, List[str]]) -> None: + """ + Check the name or shortcuts of argument(s) or flag(s). + The name must not be empty str, and must not contains \\t or \\n or \\f or \\r. + If that not satisfy the requirements, it will raise exception `IllegalArgumentName`. + 检查 参数或标记 的 名称或快捷方式 是否符合要求。 + 名称必须是非空的字符串,且不能包含 \\t 或 \\n 或 \\f 或 \\r。 + 如果不符合要求,将会抛出 `IllegalArgumentName` 异常。 + :param name: arguments + :return: None + """ + if isinstance(name, str) and EMPTY_WORDS.search(name): + raise IllegalName("The name of argument must not contains empty words.") + elif isinstance(name, list) and all((not isinstance(i, str)) and EMPTY_WORDS.search(i) for i in name): + raise IllegalName("The name of shortcut must be 'str', and must not contains empty words.") + else: + raise TypeError("The type of name must be 'str' or 'list[str]'.") + + +class Literal: + _tip = CallBackDescriptor("_tip") + _func = CallBackDescriptor("_func") + _err_callback = CallBackDescriptor("_err_callback") + + def __init__(self, name: str): + self.name: str = name + self.sub: List[Self] = [] + self._tip: Optional[CallBack] = None + self._func: Optional[CallBack] = None + self._err_callback: Optional[CallBack] = None + + self._opts: List[Option] = [] + self._args: List[Argument] = [] + self._flags: List[Flag] = [] + + def __call__(self, *nodes) -> Self: + self.sub += nodes + return self + + def __repr__(self): + attrs = (k for k in self.__dict__ if not (k.startswith("__") and k.endswith("__"))) + return f"{self.__class__.__name__}({', '.join(f'{k}={v!r}' for k in attrs if (v := self.__dict__[k]))})" + + def arg(self, name: str, types: Optional[Set[type]] = None) -> Self: + Argument(name=name, types=types) + return self + + def opt( + self, + name: str, + shortcuts: Optional[List[str]] = None, + optional: bool = True, + types: Optional[Set[type]] = None + ) -> Self: + check_name(name) + if shortcuts is not None and len(shortcuts) != 0: + check_name(shortcuts) + self._opts.append( + Option(name=name, shortcuts=shortcuts, optional=optional, types=types) + ) + return self + + def opt_group(self, opts: List[Option], exclusive: bool = False): + ... + + def flag(self, name: str, shortcuts: Optional[List[str]] = None) -> Self: + check_name(name) + if shortcuts is not None and len(shortcuts) != 0: + check_name(shortcuts) + Flag(name=name, shortcuts=shortcuts) + ... + return self + + def flag_group(self, flags: List[Flag], exclusive: bool = False) -> Self: + + ... + return self + + def error(self, callback: CallBack) -> Self: + self._err_callback = callback + return self + + def run(self, func: CallBack) -> Self: + self._func = func + return self + + def tip(self, tip: CallBack) -> Self: + self._tip = tip + return self + + def parse(self, cmd: Union[str, List[str]]) -> Parsed: + ... + + def to_doc(self) -> str: + ... + + +def builder(node: Literal) -> Literal: + ... diff --git a/libs/lib_not_dr/nuitka/__init__.py b/libs/lib_not_dr/nuitka/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/lib_not_dr/nuitka/compile.py b/libs/lib_not_dr/nuitka/compile.py new file mode 100644 index 0000000..ad7b06b --- /dev/null +++ b/libs/lib_not_dr/nuitka/compile.py @@ -0,0 +1,472 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +import platform +import warnings +from pathlib import Path +from typing import List, Tuple, Optional, Union, Any +from enum import Enum + +from lib_not_dr.types import Options, Version, VersionRequirement + + +def ensure_cmd_readable(cmd: str) -> str: + """ + 保证 参数中 不含空格 + :param cmd: 要格式化的命令行参数 + :return: 格式化后的命令行参数 + """ + if ' ' in str(cmd): + return f'"{cmd}"' + return cmd + + +def format_cmd(arg_name: Optional[str] = None, + arg_value: Optional[Union[str, List[str]]] = None, + write: Optional[Any] = True) -> List[str]: + """ + 用来格式化输出命令行参数 + :param arg_name: 类似 --show-memory 之类的主项 + :param arg_value: 类似 xxx 类的内容 + :param write: 是否写入 + :return: 直接拼接好的命令行参数 不带 = + """ + if not write: + return [] + if arg_name is None: + return [] + if arg_value is None: + return [arg_name] + if isinstance(arg_value, list): + arg_value = ','.join([ensure_cmd_readable(value) for value in arg_value]) + return [f'{arg_name}{arg_value}'] + arg_value = ensure_cmd_readable(arg_value) + return [f'{arg_name}{arg_value}'] + + +class NuitkaSubConfig(Options): + """ + Nuitka 配置的子项 + Nuitka configuration sub-items + """ + name = 'Nuitka Sub Configuration' + + def gen_cmd(self) -> List[str]: + """ + 生成命令行参数 + :return: + """ + raise NotImplementedError + + +class NuitkaPluginConfig(NuitkaSubConfig): + """ + 控制 nuitka 的 plugin 相关参数的部分 + Control part of nuitka's plugin related parameters + """ + name = 'Nuitka Plugin Configuration' + + # --enable-plugin=PLUGIN_NAME + enable_plugin: List[str] = [] + # --disable-plugin=PLUGIN_NAME + disable_plugin: List[str] = [] + # --plugin-no-detection + plugin_no_detection: bool = False + # --user-plugin=PATH + user_plugin: List[Path] = [] + # --show-source-changes + show_source_changes: bool = False + + # --include-plugin-directory=MODULE/PACKAGE + include_plugin_dir: List[str] = [] + # --include-plugin-files=PATTERN + include_plugin_files: List[str] = [] + + def gen_cmd(self) -> List[str]: + lst = [] + lst += format_cmd('--enable-plugin=', self.enable_plugin, self.enable_plugin) + lst += format_cmd('--disable-plugin=', self.disable_plugin, self.disable_plugin) + lst += format_cmd('--plugin-no-detection' if self.plugin_no_detection else None) + lst += format_cmd('--user-plugin=', [str(plugin.absolute()) for plugin in self.user_plugin], self.user_plugin) + lst += format_cmd('--show-source-changes' if self.show_source_changes else None) + lst += format_cmd('--include-plugin-directory=', self.include_plugin_dir, self.include_plugin_dir) + lst += format_cmd('--include-plugin-files=', self.include_plugin_files, self.include_plugin_files) + return lst + + +class NuitkaIncludeConfig(NuitkaSubConfig): + """ + 控制 nuitka 的 include 和 数据 相关参数的部分 + Control part of nuitka's include related parameters + """ + name = 'Nuitka Include Configuration' + + # --include-package=PACKAGE + include_packages: List[str] = [] + # --include-module=MODULE + include_modules: List[str] = [] + + # --prefer-source-code + # --no-prefer-source-code for --module + prefer_source_code: bool = False + # --follow-stdlib + follow_stdlib: bool = False + + def gen_cmd(self) -> List[str]: + lst = [] + lst += format_cmd('--include-package=', self.include_packages, self.include_packages) + lst += format_cmd('--include-module=', self.include_modules, self.include_modules) + lst += format_cmd('--prefer-source-code' if self.prefer_source_code else None) + lst += format_cmd('--no-prefer-source-code' if not self.prefer_source_code else None) + lst += format_cmd('--follow-stdlib' if self.follow_stdlib else None) + return lst + + +class NuitkaDataConfig(NuitkaSubConfig): + """ + 控制 nuitka 的 数据 相关参数的部分 + Control part of nuitka's data related parameters + """ + name = 'Nuitka Data Configuration' + + # --include-package-data=PACKAGE=PACKAGE_PATH + include_package_data: List[Tuple[Path, Path]] = [] + # --include-data-files=PATH=PATH + include_data_files: List[Tuple[Path, Path]] = [] + # --include-data-dir=DIRECTORY=PATH + include_data_dir: List[Tuple[Path, Path]] = [] + + # --noinclude-data-files=PATH + no_include_data_files: List[Path] = [] + + # --list-package-data=LIST_PACKAGE_DATA + list_package_data: List[str] = [] + # --list-package-dlls=LIST_PACKAGE_DLLS + list_package_dlls: List[str] = [] + + # --include-distribution-metadata=DISTRIBUTION + include_distribution_metadata: List[str] = [] + + +class NuitkaBinaryInfo(Options): + """ + nuitka 构建的二进制文件的信息 + nuitka build binary file information + """ + name = 'Nuitka Binary Info' + + # --company-name=COMPANY_NAME + company_name: Optional[str] = None + # --product-name=PRODUCT_NAME + product_name: Optional[str] = None + + # --file-version=FILE_VERSION + # --macos-app-version=MACOS_APP_VERSION + file_version: Optional[Union[str, Version]] = None + # --product-version=PRODUCT_VERSION + product_version: Optional[Union[str, Version]] = None + + # --file-description=FILE_DESCRIPTION + file_description: Optional[str] = None + # --copyright=COPYRIGHT_TEXT + copyright: Optional[str] = None + # --trademarks=TRADEMARK_TEXT + trademarks: Optional[str] = None + + # Icon + # --linux-icon=ICON_PATH + # --macos-app-icon=ICON_PATH + # --windows-icon-from-ico=ICON_PATH + # --windows-icon-from-exe=ICON_EXE_PATH + # 注意: 只有 Windows 下 才可以提供多个 ICO 文件 + # 其他平台 和 EXE 下只会使用第一个路径 + icon: Optional[List[Path]] = None + + # Console + # --enable-console + # --disable-console + console: bool = True + + # Windows UAC + # --windows-uac-admin + windows_uac_admin: bool = False + # --windows-uac-uiaccess + windows_uac_ui_access: bool = False + + +class NuitkaOutputConfig(Options): + """ + nuitka 构建的选项 + nuitka build output information + """ + name = 'Nuitka Output Config' + + # --output-dir=DIRECTORY + output_dir: Optional[Path] = None + # --output-filename=FILENAME + output_filename: Optional[str] = None + + # --quiet + quiet: bool = False + # --no-progressbar + no_progressbar: bool = False + # --verbose + verbose: bool = False + # --verbose-output=PATH + verbose_output: Optional[Path] = None + + # --show-progress + show_progress: bool = False + # --show-memory + show_memory: bool = False + # --show-scons + show_scons: bool = False + # --show-modules + show_modules: bool = False + # --show-modules-output=PATH + show_modules_output: Optional[Path] = None + + # --xml=XML_FILENAME + xml: Optional[Path] = None + # --report=REPORT_FILENAME + report: Optional[Path] = None + # --report-diffable + report_diffable: bool = False + + # --remove-output + remove_output: bool = False + # --no-pyo-file + no_pyo_file: bool = False + + +class NuitkaDebugConfig(Options): + """ + nuitka 构建的调试选项 + nuikta build debug information + """ + name = 'Nuitka Debug Config' + + # --debug + debug: bool = False + # --unstripped + strip: bool = True + # --profile + profile: bool = False + # --internal-graph + internal_graph: bool = False + # --trace-execution + trace_execution: bool = False + # --recompile-c-only + recompile_c_only: bool = False + # --generate-c-only + generate_c_only: bool = False + # --deployment + deployment: bool = False + # --no-deployment-flag=FLAG + deployment_flag: Optional[str] = None + # --experimental=FLAG + experimental: Optional[str] = None + + +class NuitkaTarget(Enum): + """ + 用于指定 nuitka 构建的目标 + Use to specify the target of nuitka build + exe: 不带任何参数 + module: --module + standalone: --standalone + one_file: --onefile + """ + exe = '' + module = 'module' + standalone = 'standalone' + one_file = 'package' + + +class NuitkaScriptGenerator(Options): + """ + 用于帮助生成 nuitka 构建脚本的类 + Use to help generate nuitka build script + + :arg main 需要编译的文件 + """ + name = 'Nuitka Script Generator' + + # --main=PATH + # 可以有多个 输入时需要包在列表里 + main: List[Path] + + # --run + run_after_build: bool = False + # --debugger + debugger: bool = False + # --execute-with-pythonpath + execute_with_python_path: bool = False + + # --assume-yes-for-downloads + download_confirm: bool = True + + # standalone/one_file/module/exe + target: NuitkaTarget = NuitkaTarget.exe + + # --python-debug + python_debug: bool = False + # --python-flag=FLAG + python_flag: List[str] = [] + # --python-for-scons=PATH + python_for_scons: Optional[Path] = None + + +class CompilerHelper(Options): + """ + 用于帮助生成 nuitka 构建脚本的类 + Use to help generate nuitka build script + + """ + name = 'Nuitka Compiler Helper' + + output_path: Path = Path('./build') + src_file: Path + + python_cmd: str = 'python' + compat_nuitka_version: VersionRequirement = VersionRequirement("~1.8.0") # STATIC VERSION + + # 以下为 nuitka 的参数 + # nuitka options below + use_lto: bool = False # --lto=yes (no is faster) + use_clang: bool = True # --clang + use_msvc: bool = True # --msvc=latest + use_mingw: bool = False # --mingw64 + + onefile: bool = False # --onefile + onefile_tempdir: Optional[str] = '' # --onefile-tempdir-spec= + standalone: bool = True # --standalone + use_ccache: bool = True # not --disable-ccache + enable_console: bool = True # --enable-console / --disable-console + + show_progress: bool = True # --show-progress + show_memory: bool = False # --show-memory + remove_output: bool = True # --remove-output + save_xml: bool = False # --xml + xml_path: Path = Path('build/compile_data.xml') + save_report: bool = False # --report + report_path: Path = Path('build/compile_report.xml') + + download_confirm: bool = True # --assume-yes-for-download + run_after_build: bool = False # --run + + company_name: Optional[str] = '' + product_name: Optional[str] = '' + file_version: Optional[Version] = None + product_version: Optional[Version] = None + file_description: Optional[str] = '' # --file-description + + copy_right: Optional[str] = '' # --copyright + + icon_path: Optional[Path] = None + + follow_import: List[str] = [] + no_follow_import: List[str] = [] + + include_data_dir: List[Tuple[str, str]] = [] + include_packages: List[str] = [] + + enable_plugin: List[str] = [] # --enable-plugin=xxx,xxx + disable_plugin: List[str] = [] # --disable-plugin=xxx,xxx + + def init(self, **kwargs) -> None: + if (compat_version := kwargs.get('compat_nuitka_version')) is not None: + if not self.compat_nuitka_version.accept(compat_version): + warnings.warn( + f"Nuitka version may not compat with {compat_version}\n" + "requirement: {self.compat_nuitka_version}" + ) + # 非 windows 平台不使用 msvc + if platform.system() != 'Windows': + self.use_msvc = False + self.use_mingw = False + else: + self.use_mingw = self.use_mingw and not self.use_msvc + # Windows 平台下使用 msvc 时不使用 mingw + + def __str__(self): + return self.as_markdown() + + def as_markdown(self, longest: Optional[int] = None) -> str: + """ + 输出编译器帮助信息 + Output compiler help information + + Args: + longest (Optional[int], optional): + 输出信息的最大长度限制 The maximum length of output information. + Defaults to None. + + Returns: + str: 以 markdown 格式输出的编译器帮助信息 + Compile helper information in markdown format + """ + front = super().as_markdown(longest) + gen_cmd = self.gen_subprocess_cmd() + return f"{front}\n\n```bash\n{' '.join(gen_cmd)}\n```" + + def gen_subprocess_cmd(self) -> List[str]: + """生成 nuitka 构建脚本 + Generate nuitka build script + + Returns: + List[str]: + 生成的 nuitka 构建脚本 + Generated nuitka build script + """ + cmd_list = [self.python_cmd, '-m', 'nuitka'] + # macos 和 非 macos icon 参数不同 + if platform.system() == 'Darwin': + cmd_list += format_cmd('--macos-app-version=', self.product_version, self.product_version) + cmd_list += format_cmd('--macos-app-icon=', self.icon_path.absolute(), self.icon_path) + elif platform.system() == 'Windows': + cmd_list += format_cmd('--windows-icon-from-ico=', self.icon_path.absolute(), self.icon_path) + elif platform.system() == 'Linux': + cmd_list += format_cmd('--linux-icon=', self.icon_path.absolute(), self.icon_path) + + cmd_list += format_cmd('--lto=', 'yes' if self.use_lto else 'no') + cmd_list += format_cmd('--clang' if self.use_clang else None) + cmd_list += format_cmd('--msvc=latest' if self.use_msvc else None) + cmd_list += format_cmd('--mingw64' if self.use_mingw else None) + cmd_list += format_cmd('--standalone' if self.standalone else None) + cmd_list += format_cmd('--onefile' if self.onefile else None) + cmd_list += format_cmd('--onefile-tempdir-spec=', self.onefile_tempdir, self.onefile_tempdir) + + cmd_list += format_cmd('--disable-ccache' if not self.use_ccache else None) + cmd_list += format_cmd('--show-progress' if self.show_progress else None) + cmd_list += format_cmd('--show-memory' if self.show_memory else None) + cmd_list += format_cmd('--remove-output' if self.remove_output else None) + cmd_list += format_cmd('--assume-yes-for-download' if self.download_confirm else None) + cmd_list += format_cmd('--run' if self.run_after_build else None) + cmd_list += format_cmd('--enable-console' if self.enable_console else '--disable-console') + + cmd_list += format_cmd('--xml=', str(self.xml_path.absolute()), self.save_xml) + cmd_list += format_cmd('--report=', str(self.report_path.absolute()), self.save_report) + cmd_list += format_cmd('--output-dir=', str(self.output_path.absolute()), self.output_path) + cmd_list += format_cmd('--company-name=', self.company_name, self.company_name) + cmd_list += format_cmd('--product-name=', self.product_name, self.product_name) + cmd_list += format_cmd('--file-version=', str(self.file_version), self.file_version) + cmd_list += format_cmd('--product-version=', str(self.product_version), self.product_version) + cmd_list += format_cmd('--file-description=', self.file_description, self.file_description) + cmd_list += format_cmd('--copyright=', self.copy_right, self.copy_right) + + cmd_list += format_cmd('--follow-import-to=', self.follow_import, self.follow_import) + cmd_list += format_cmd('--nofollow-import-to=', self.no_follow_import, self.no_follow_import) + cmd_list += format_cmd('--enable-plugin=', self.enable_plugin, self.enable_plugin) + cmd_list += format_cmd('--disable-plugin=', self.disable_plugin, self.disable_plugin) + + if self.include_data_dir: + cmd_list += [f"--include-data-dir={src}={dst}" for src, dst in self.include_data_dir] + if self.include_packages: + cmd_list += [f"--include-package={package}" for package in self.include_packages] + + cmd_list.append(f"--main={self.src_file}") + return cmd_list diff --git a/libs/lib_not_dr/types/__init__.py b/libs/lib_not_dr/types/__init__.py new file mode 100644 index 0000000..0717150 --- /dev/null +++ b/libs/lib_not_dr/types/__init__.py @@ -0,0 +1,31 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +from .options import (Options, + OptionsError, + OptionNotFound, + OptionNameNotDefined, + get_type_hints_) + +from .version import (Version, + VersionRequirement, + VersionParsingError, + ExtraElement) + +__all__ = [ + # options + 'get_type_hints_', + 'Options', + 'OptionsError', + 'OptionNotFound', + 'OptionNameNotDefined', + + # version + 'Version', + 'VersionRequirement', + 'VersionParsingError', + 'ExtraElement' +] diff --git a/libs/lib_not_dr/types/options.py b/libs/lib_not_dr/types/options.py new file mode 100644 index 0000000..5ae313e --- /dev/null +++ b/libs/lib_not_dr/types/options.py @@ -0,0 +1,271 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +import shutil +import traceback +from io import StringIO +from typing import get_type_hints, Type, List, Union, Dict, Any, Callable, Tuple, Optional, TYPE_CHECKING, Iterable + +__all__ = [ + 'get_type_hints_', + 'Options', + 'OptionsError', + 'OptionNotFound', + 'OptionNameNotDefined' +] + + +def get_type_hints_(cls: Type): + try: + return get_type_hints(cls) + except ValueError: + return get_type_hints(cls, globalns={}) + + +def to_str_value_(value: Any) -> Any: + """递归的将输入值的每一个非 builtin type 转换成 str""" + if isinstance(value, (str, bytes, bytearray, int, float, bool, type(None))): + return value + elif isinstance(value, dict): + return {k: to_str_value_(v) for k, v in value.items()} + elif isinstance(value, (list, Iterable)): + return [to_str_value_(v) for v in value] + else: + return str(value) + + +class OptionsError(Exception): + """ option 的错误基类""" + + +class OptionNameNotDefined(OptionsError): + """ 向初始化的 option 里添加了一个不存在于选项里的选项 """ + + +class OptionNotFound(OptionsError): + """ 某个选项没有找到 """ + + +class Options: + """ + 一个用于存储选项 / 提供 API 定义 的类 + 用法: + 存储配置: 继承 Options 类 + 在类里定义 option: typing + (可选 定义 name: str = 'Option Base' 用于在打印的时候显示名字) + 提供 API 接口: 继承 Options 类 + 在类里定义 option: typing + 定义 一些需要的方法 + 子类: 继承 新的 Options 类 + 实现定义的方法 + """ + name = 'Option Base' + cached_options: Dict[str, Union[str, Any]] = {} + + def __init__(self, **kwargs): + """ + 创建一个新的 Options 的时候的配置 + 如果存在 init 方法 会在设置完 kwargs 之后运行子类的 init 方法 + :param kwargs: 需要设置的选项 + """ + if TYPE_CHECKING: + self._options: Dict[str, Union[Callable, object]] = {} + self.flush_option() + for option, value in kwargs.items(): + if option not in self.cached_options: + raise OptionNameNotDefined(f"option: {option} with value: {value} is not defined") + setattr(self, option, value) + run_load_file = True + if hasattr(self, 'init'): + run_load_file = self.init(**kwargs) # 默认 False/None + run_load_file = not run_load_file + if hasattr(self, 'load_file') and run_load_file: + try: + self.load_file() + except Exception: + traceback.print_exc() + self.flush_option() + + def __str__(self): + return f"<{self.__class__.__name__} {self.name}>" if self.name else f"<{self.__class__.__name__}>" + + def __repr__(self): + return self.__str__() + + if TYPE_CHECKING: + _options: Dict[str, Union[Callable, object]] = {} + + def init(self, **kwargs) -> bool: + """ 如果子类定义了这个函数,则会在 __init__ 之后调用这个函数 + 返回值为 True 则不会调用 load_file 函数 + """ + + def load_file(self) -> bool: + """如果子类定义了这个函数,则会在 __init__ 和 init 之后再调用这个函数 + + 请注意,这个函数请尽量使用 try 包裹住可能出现错误的部分 + 否则会在控制台输出你的报错""" + return True + + def option(self) -> Dict[str, Any]: + """ + 获取配置类的所有配置 + :return: 自己的所有配置 + """ + values = {} + for ann in self.__annotations__: # 获取类型注释 + values[ann] = getattr(self, ann, None) + if values[ann] is None: + values[ann] = self.__annotations__[ann] + + if not hasattr(self, '_options'): + self._options: Dict[str, Union[Callable, object]] = {} + for option, a_fun in self._options.items(): # 获取额外内容 + values[option] = a_fun + + for option, a_fun in values.items(): # 检查是否为 property + if a_fun is bool and getattr(self, option, None) is not None: + values[option] = False + if isinstance(a_fun, property): + try: + values[option] = getattr(self, option) + except AttributeError: + raise OptionNotFound(f'Option {option} is not found in {self.name}') from None + return values + + def str_option(self, shrink_to_long: Optional[int] = None) -> Dict[str, Union[str, Any]]: + """ + 获取配置类的所有配置 并将所有非 BuiltIn 类型的值转换为 str + :return: + """ + raw_option = self.option() + str_option = to_str_value_(raw_option) + if shrink_to_long is None: + return str_option + if not isinstance(shrink_to_long, int) or shrink_to_long <= 0: + return str_option + for option, value in str_option.items(): + if value is not None: + if len(str(value)) > shrink_to_long: + str_option[option] = str(value)[:shrink_to_long] + '...' + return str_option + + def format(self, text: str) -> str: + """ + 使用自己的选项给输入的字符串替换内容 + :param text: 想替换的内容 + :return: 替换之后的内容 + """ + cache_option = self.flush_option() + for option, value in cache_option.items(): + text = text.replace(f'{{{option}}}', str(value)) + return text + + def flush_option(self) -> Dict[str, Any]: + """ + 刷新缓存 options 的内容 + :return: 刷新过的 options + """ + self.cached_options = self.option() + return self.cached_options + + def option_with_len(self) -> Tuple[List[Tuple[str, Any, Type]], int, int, int]: + """ + 返回一个可以用于打印的 option 列表 + :return: + """ + options = self.flush_option() + max_len_key = 1 + max_len_value = 1 + max_len_value_t = 1 + option_list = [] + for key, value in options.items(): + value_t = type(value) if isinstance(value, type(value)) else type(value) # 判定这个类型 是不是 基本类型 + max_len_key = max(max_len_key, len(key)) + max_len_value = max(max_len_value, len(str(value))) + max_len_value_t = max(max_len_value_t, len(str(value_t))) + option_list.append([key, value, value_t]) + return [option_list, max_len_key, max_len_value, max_len_value_t] # noqa + + def as_markdown(self, longest: Optional[int] = None) -> str: + """ + 返回一个 markdown 格式的 option 字符串 + :param longest: 最长的输出长度 + :return: markdown 格式的 option 字符串 + """ + value = self.option_with_len() + cache = StringIO() + option_len = max(value[1], len('Option')) + value_len = max(value[2], len('Value')) + value_type_len = max(value[3], len('Value Type')) + + # | Option | Value | Value Type | + shortest = len('Option | Value | Value Type') + + if longest is not None: + console_width = max(longest, shortest) + else: + console_width = shutil.get_terminal_size(fallback=(100, 80)).columns + console_width = max(console_width, shortest) + + # 为每一栏 预分配 1/3 或者 需要的宽度 (如果不需要 1/3) + option_len = min(option_len, console_width // 3) + value_len = min(value_len, console_width // 3) + value_type_len = min(value_type_len, console_width // 3) + + # 先指定每一个列的输出最窄宽度, 然后去尝试增加宽度 + # 循环分配新空间之前 首先检查是否已经不需要多分配 (and 后面) + while option_len + value_len + value_type_len + 16 < console_width\ + and (option_len < value[1] + or value_len < value[2] + or value_type_len < value[3]): + # 每一个部分的逻辑都是 + # 如果现在的输出长度小于原始长度 + # 并且长度 + 1 之后的总长度依然在允许范围内 + # 那么就 + 1 + if option_len < value[1] and option_len + value_len + value_type_len + 16 < console_width: + option_len += 1 + if value_len < value[2] and option_len + value_len + value_type_len + 16 < console_width: + value_len += 1 + if value_type_len < value[3] and option_len + value_len + value_type_len + 16 < console_width: + value_type_len += 1 + # 实际上 对于列表(可变对象) for 出来的这个值是一个引用 + # 所以可以直接修改 string + for v in value[0]: + if len(str(v[0])) > option_len: + v[0] = f'{str(v[0])[:value_len - 3]}...' + if len(str(v[1])) > value_len: + v[1] = f'{str(v[1])[:value_len - 3]}...' + if len(str(v[2])) > value_type_len: + v[2] = f'{str(v[2])[:value_len - 3]}..' + + cache.write( + f"| Option{' ' * (option_len - 3)}| Value{' ' * (value_len - 2)}| Value Type{' ' * (value_type_len - 7)}|\n") + cache.write(f'|:{"-" * (option_len + 3)}|:{"-" * (value_len + 3)}|:{"-" * (value_type_len + 3)}|\n') + for option, value, value_t in value[0]: + cache.write(f"| `{option}`{' ' * (option_len - len(option))} " + f"| `{value}`{' ' * (value_len - len(str(value)))} " + f"| `{value_t}`{' ' * (value_type_len - len(str(value_t)))} |\n") + result = cache.getvalue() + cache.close() + return result + + @classmethod + def add_option(cls, name: str, value: Union[Callable, object]) -> Dict: + """ + 向配置类中添加一个额外的配置 + :param name: 配置的名字 + :param value: 用于获取配置的函数或者类 + :return: 配置类的所有配置 + """ + if not hasattr(cls, '_options'): + cls._options: Dict[str, Union[Callable, object]] = {} + cls._options[name] = value + return cls._options + + @staticmethod + def init_option(options_class: Type['Options'], init_value: Optional[dict] = None) -> 'Options': + return options_class(**init_value if init_value is not None else {}) diff --git a/libs/lib_not_dr/types/version.py b/libs/lib_not_dr/types/version.py new file mode 100644 index 0000000..bbf3e42 --- /dev/null +++ b/libs/lib_not_dr/types/version.py @@ -0,0 +1,220 @@ +# 本文件以 GNU Lesser General Public License v3.0(GNU LGPL v3) 开源协议进行授权 (谢谢狐狸写出这么好的MCDR) +# 顺便说一句,我把所有的tab都改成了空格,因为我觉得空格比tab更好看(草,后半句是github copilot自动填充的) + +""" +This part of code come from MCDReforged(https://github.com/Fallen-Breath/MCDReforged) +Thanks a lot to Fallen_Breath and MCDR contributors +GNU Lesser General Public License v3.0 (GNU LGPL v3) +""" + +import re +from typing import List, Callable, Tuple, Optional, Union +""" +Plugin Version +""" + + +# beta.3 -> (beta, 3), random -> (random, None) +class ExtraElement: + DIVIDER = '.' + body: str + num: Optional[int] + + def __init__(self, segment_str: str): + segments = segment_str.rsplit(self.DIVIDER, 1) + try: + self.body, self.num = segments[0], int(segments[1]) + except (IndexError, ValueError): + self.body, self.num = segment_str, None + + def __str__(self): + if self.num is None: + return self.body + return '{}{}{}'.format(self.body, self.DIVIDER, self.num) + + def __lt__(self, other): + if not isinstance(other, type(self)): + raise TypeError() + if self.num is None or other.num is None: + return str(self) < str(other) + else: + return (self.body, self.num) < (other.body, other.num) + + +class Version: + """ + A version container that stores semver like version string + + Example: + + * ``"1.2.3"`` + * ``"1.0.*"`` + * ``"1.2.3-pre4+build.5"`` + """ + EXTRA_ID_PATTERN = re.compile(r'|[-+0-9A-Za-z]+(\.[-+0-9A-Za-z]+)*') + WILDCARDS = ('*', 'x', 'X') + WILDCARD = -1 + + component: List[int] + has_wildcard: bool + pre: Optional[ExtraElement] + build: Optional[ExtraElement] + + def __init__(self, version_str: str, *, allow_wildcard: bool = True): + """ + :param version_str: The version string to be parsed + :keyword allow_wildcard: If wildcard (``"*"``, ``"x"``, ``"X"``) is allowed. Default: ``True`` + """ + if not isinstance(version_str, str): + raise VersionParsingError('Invalid input version string') + + def separate_extra(text, char) -> Tuple[str, Optional[ExtraElement]]: + if char in text: + text, extra_str = text.split(char, 1) + if not self.EXTRA_ID_PATTERN.fullmatch(extra_str): + raise VersionParsingError('Invalid build string: ' + extra_str) + extra = ExtraElement(extra_str) + else: + extra = None + return text, extra + + self.component = [] + self.has_wildcard = False + version_str, self.build = separate_extra(version_str, '+') + version_str, self.pre = separate_extra(version_str, '-') + if len(version_str) == 0: + raise VersionParsingError('Version string is empty') + for comp in version_str.split('.'): + if comp in self.WILDCARDS: + self.component.append(self.WILDCARD) + self.has_wildcard = True + if not allow_wildcard: + raise VersionParsingError('Wildcard {} is not allowed'.format(comp)) + else: + try: + num = int(comp) + except ValueError: + num = None + if num is None: + raise VersionParsingError('Invalid version number component: {}'.format(comp)) + if num < 0: + raise VersionParsingError('Unsupported negatived number component: {}'.format(num)) + self.component.append(num) + if len(self.component) == 0: + raise VersionParsingError('Empty version string') + + def __str__(self): + version_str = '.'.join(map(lambda c: str(c) if c != self.WILDCARD else self.WILDCARDS[0], self.component)) + if self.pre is not None: + version_str += '-' + str(self.pre) + if self.build is not None: + version_str += '+' + str(self.build) + return version_str + + def __repr__(self): + return self.__str__() + + def __getitem__(self, index: int) -> int: + if index < len(self.component): + return self.component[index] + else: + return self.WILDCARD if self.component[len(self.component) - 1] == self.WILDCARD else 0 + + def __lt__(self, other): + if not isinstance(other, Version): + raise TypeError('Cannot compare between instances of {} and {}'.format(Version.__name__, type(other).__name__)) + for i in range(max(len(self.component), len(other.component))): + if self[i] == self.WILDCARD or other[i] == self.WILDCARD: + continue + if self[i] != other[i]: + return self[i] < other[i] + if self.pre is not None and other.pre is not None: + return self.pre < other.pre + elif self.pre is not None: + return not other.has_wildcard + elif other.pre is not None: + return False + else: + return False + + def __eq__(self, other): + return not self < other and not other < self + + def __le__(self, other): + return self == other or self < other + + def compare_to(self, other): + if self < other: + return -1 + elif self > other: + return 1 + else: + return 0 + + +DEFAULT_CRITERION_OPERATOR = '=' + + +class Criterion: + def __init__(self, opt: str, base_version: Version, criterion: Callable[[Version, Version], bool]): + self.opt = opt + self.base_version = base_version + self.criterion = criterion + + def test(self, target: Union[Version, str]): + return self.criterion(self.base_version, target) + + def __str__(self): + return '{}{}'.format(self.opt if self.opt != DEFAULT_CRITERION_OPERATOR else '', self.base_version) + + +class VersionRequirement: + """ + A version requirement tester + + It can test if a given :class:`Version` object matches its requirement + """ + CRITERIONS = { + '<=': lambda base, ver: ver <= base, + '>=': lambda base, ver: ver >= base, + '<': lambda base, ver: ver < base, + '>': lambda base, ver: ver > base, + '=': lambda base, ver: ver == base, + '^': lambda base, ver: ver >= base and ver[0] == base[0], + '~': lambda base, ver: ver >= base and ver[0] == base[0] and ver[1] == base[1], + } + + def __init__(self, requirements: str): + """ + :param requirements: The requirement string, which contains several version predicates connected by space character. + e.g. ``">=1.0.x"``, ``"^2.9"``, ``">=1.2.0 <1.4.3"`` + """ + if not isinstance(requirements, str): + raise VersionParsingError('Requirements should be a str, not {}'.format(type(requirements).__name__)) + self.criterions = [] # type: List[Criterion] + for requirement in requirements.split(' '): + if len(requirement) > 0: + for prefix, func in self.CRITERIONS.items(): + if requirement.startswith(prefix): + opt = prefix + base_version = requirement[len(prefix):] + break + else: + opt = DEFAULT_CRITERION_OPERATOR + base_version = requirement + self.criterions.append(Criterion(opt, Version(base_version), self.CRITERIONS[opt])) + + def accept(self, version: Union[Version, str]): + if isinstance(version, str): + version = Version(version) + for criterion in self.criterions: + if not criterion.test(version): + return False + return True + + def __str__(self): + return ' '.join(map(str, self.criterions)) + + +class VersionParsingError(ValueError): + pass