Skip to content

框架工具

正如前述的 Commander 样例中,一个 Commander APP 需要被设定一个入口,以便于它被注册至全局命令亦或者方便从 Pypi 安装。

因此,一个经典的 Commander APP 的目录结构为(以 test 为例):

sh
📂 .
├── 📒 project_configure.json
├── 📒 README.md
├── 📂 dist
   └── 📒 input.txt
├── 📂 template
   └── 📒 main
└── 📂 test
    ├── 📒 __config__.py
    ├── 📒 __init__.py
    └── 📒 main.py
📂 .
├── 📒 project_configure.json
├── 📒 README.md
├── 📂 dist
   └── 📒 input.txt
├── 📂 template
   └── 📒 main
└── 📂 test
    ├── 📒 __config__.py
    ├── 📒 __init__.py
    └── 📒 main.py

它的配置表内容为

json
{
  "build": "",
  "entry_point": "test/main.py",
  "executable": "python3 -m test.main",
  "input_file": "dist/input.txt",
  "template_root": "template/",
  "server_targets": [
    {
      "user": "",
      "host": "",
      "port": 22,
      "path": ""
    }
  ],
  "enable_complete": true
}
{
  "build": "",
  "entry_point": "test/main.py",
  "executable": "python3 -m test.main",
  "input_file": "dist/input.txt",
  "template_root": "template/",
  "server_targets": [
    {
      "user": "",
      "host": "",
      "port": 22,
      "path": ""
    }
  ],
  "enable_complete": true
}

接下来将阐述test包下的内容。

test/__config__.py

此文件是 Commander 模板提供,用于帮助命令行 APP 在本地存储配置信息的部分。它的内容为:

python
import os
import json
from QuickProject import user_root, user_lang, QproDefaultConsole, QproInfoString, _ask

enable_config = False
config_path = os.path.join(user_root, ".test_config")

questions = {
    'name': {
        'type': 'input',
        'message': 'What is your name?',
    },
}

def init_config():
    with open(config_path, "w") as f:
        json.dump({i: _ask(questions[i]) for i in questions}, f, indent=4, ensure_ascii=False)
    QproDefaultConsole.print(QproInfoString, f'Config file has been created at: "{config_path}"' if user_lang != 'zh' else f'配置文件已创建于: "{config_path}"')


class testConfig:
    def __init__(self):
        if not os.path.exists(config_path):
            init_config()
        with open(config_path, "r") as f:
            self.config = json.load(f)

    def select(self, key):
        if key not in self.config and key in questions:
            self.update(key, _ask(questions[key]))
        return self.config[key]

    def update(self, key, value):
        self.config[key] = value
        with open(config_path, "w") as f:
            json.dump(self.config, f, indent=4, ensure_ascii=False)
import os
import json
from QuickProject import user_root, user_lang, QproDefaultConsole, QproInfoString, _ask

enable_config = False
config_path = os.path.join(user_root, ".test_config")

questions = {
    'name': {
        'type': 'input',
        'message': 'What is your name?',
    },
}

def init_config():
    with open(config_path, "w") as f:
        json.dump({i: _ask(questions[i]) for i in questions}, f, indent=4, ensure_ascii=False)
    QproDefaultConsole.print(QproInfoString, f'Config file has been created at: "{config_path}"' if user_lang != 'zh' else f'配置文件已创建于: "{config_path}"')


class testConfig:
    def __init__(self):
        if not os.path.exists(config_path):
            init_config()
        with open(config_path, "r") as f:
            self.config = json.load(f)

    def select(self, key):
        if key not in self.config and key in questions:
            self.update(key, _ask(questions[key]))
        return self.config[key]

    def update(self, key, value):
        self.config[key] = value
        with open(config_path, "w") as f:
            json.dump(self.config, f, indent=4, ensure_ascii=False)

从引入相关包以后,可以选择:

  1. 项目是否启用配置文件(默认为不启用)
  2. 设置配置文件存储位置(默认为用户根目录下的.<项目名>_config,并以 json 格式存储)
  3. 自定义问题。

自定义问题

如上述代码所见,在questions字典中加入你需要在配置表存储的键值对,即可添加问题。

问题的类型可以参照此问题样例来添加,常见的问题种类包括input(用户自行输入一串文本)、list(单选)、checkbox(多选)、confirm(是或否)。

test/__init__.py

此文件提供若干基础 API,比如引用依赖的询问安装,执行命令等,如果启用了配置表,则可全局使用config对象来获取配置表信息。可自定义此文件内容,以便->from . import balabala

动态引用

QuickProject.Commander 提供动态引入功能,下面将通过使用样例来展示它的功能。

python
from . import requirePackage

img = requirePackage('PIL', 'Image', real_name='Pillow').open('1.png') # 当`PIL`库不存在时,将会提示用户是否通过`pip`安装 Pillow。

pyperclip = requirePackage('pyperclip') # 直接返回pyperclip包
pyperclip.copy('test')

copy = requirePackage('pyperclip', 'copy') # 可以直接获取函数
copy('test')

content_in_clipboard = requirePackage('pyperclip', 'paste')() # content_in_clipboard = test

shell_executor = requirePackage('.', 'external_exec') # 支持库内相对引用
from . import requirePackage

img = requirePackage('PIL', 'Image', real_name='Pillow').open('1.png') # 当`PIL`库不存在时,将会提示用户是否通过`pip`安装 Pillow。

pyperclip = requirePackage('pyperclip') # 直接返回pyperclip包
pyperclip.copy('test')

copy = requirePackage('pyperclip', 'copy') # 可以直接获取函数
copy('test')

content_in_clipboard = requirePackage('pyperclip', 'paste')() # content_in_clipboard = test

shell_executor = requirePackage('.', 'external_exec') # 支持库内相对引用

test/main.py

此文件是命令行 APP 的入口文件,它的结构如下

python
from QuickProject.Commander import Commander
from . import *

app = Commander(name)


@app.command()
def hello(name: str):
    """
    echo Hello <name>

    :param name: str
    """
    print(f"Hello {name}!")


def main():
    """
    注册为全局命令时, 默认采用main函数作为命令入口, 请勿将此函数用作它途.
    When registering as a global command, default to main function as the command entry, do not use it as another way.
    """
    app()


if __name__ == '__main__':
    main()
from QuickProject.Commander import Commander
from . import *

app = Commander(name)


@app.command()
def hello(name: str):
    """
    echo Hello <name>

    :param name: str
    """
    print(f"Hello {name}!")


def main():
    """
    注册为全局命令时, 默认采用main函数作为命令入口, 请勿将此函数用作它途.
    When registering as a global command, default to main function as the command entry, do not use it as another way.
    """
    app()


if __name__ == '__main__':
    main()

你可以实现多个函数,并用@app.command()来装饰它们,将它们变为 Commander APP 的子命令。

在调用方面,必填参数需要按顺序填写、可选参数需要以--<参数名> balabala方式设置。

重要提示

Commander 不会支持解析 dict 和 set 类型的参数。

自定义前置操作

您可能有一类子命令,它们的参数表一样,并且在开始工作前会先进行几乎一致的判断步骤(比如某文件是否存在、某前提是否满足等等),重复写这些逻辑是很烦人的,因此 Commander 支持绑定前置函数来实现。

app()被调用前,添加你需要绑定的函数:

WARNING

前置函数的参数表需要与被绑定的子命令参数表保持一致,且前置函数需要返回 bool 类型来表示是否验证成功。

python
from QuickProject.Commander import Commander
from . import *

app = Commander(name)

def check_hello(name: str):
    return name in ['Alice', "Bob", "Candy", "David"]

@app.command()
def hello(name: str):
    """
    echo Hello <name>

    :param name: str
    """
    print(f"Hello {name}!")


def main():
    """
    注册为全局命令时, 默认采用main函数作为命令入口, 请勿将此函数用作它途.
    When registering as a global command, default to main function as the command entry, do not use it as another way.
    """
    app.bind_pre_call('hello', check_hello)
    app()


if __name__ == '__main__':
    main()
from QuickProject.Commander import Commander
from . import *

app = Commander(name)

def check_hello(name: str):
    return name in ['Alice', "Bob", "Candy", "David"]

@app.command()
def hello(name: str):
    """
    echo Hello <name>

    :param name: str
    """
    print(f"Hello {name}!")


def main():
    """
    注册为全局命令时, 默认采用main函数作为命令入口, 请勿将此函数用作它途.
    When registering as a global command, default to main function as the command entry, do not use it as another way.
    """
    app.bind_pre_call('hello', check_hello)
    app()


if __name__ == '__main__':
    main()

调用其他子命令

当您想在当前子命令的实现中,调用之前实现的子命令,是无法通过直接调用被装饰的函数来实现的(因为它被@app.command()装饰起来了)。

因此可以:app.real_call('command', *args, **kwargs)来调用。

隐藏子命令

对于某些不常用且复杂的子命令,我们不希望它总是在--help菜单里占据位置,因此我们可以暂时隐藏它:

python

@app.command(True)
def hidden_function(a, b, c, d, e, f, g):
    pass

@app.command(True)
def hidden_function(a, b, c, d, e, f, g):
    pass

此时,运行<qrun或某命令> --help时,hidden_function将不会出现在帮助菜单中;仅当用户使用未定义的子命令时,完整的菜单才会展示。

WARNING

Commander 对象在被创建时,默认会注册一个隐藏的complete函数,用于帮助用户生成补全脚本并安装至 fig。

自定义补全提示

APP 某个子命令的参数可能是几个固定值中的一个,您可能希望在补全时直接提示这几个候选项。

python
from QuickProject.Commander import Commander
from . import *

app = Commander()


@app.custom_complete('name')
def hello():
    return ['Alice', "Bob", "Candy", "David"]
    # 或者
    return [
        {
            'name': 'Alice',
            'description': 'Alice',
            'icon': '👩'
        },
        {
            'name': 'Bob',
            'description': 'Bob',
            'icon': '👨'
        },
        {
            'name': 'Candy',
            'description': 'Candy',
            'icon': '👧'
        },
        {
            'name': 'David',
            'description': 'David',
            'icon': '👦'
        },
    ]


@app.command()
def hello(name: str):
    """
    echo Hello <name>

    :param name: str
    """
    print(f"Hello {name}!")


def main():
    """
    注册为全局命令时, 默认采用main函数作为命令入口, 请勿将此函数用作它途.
    When registering as a global command, default to main function as the command entry, do not use it as another way.
    """
    app()


if __name__ == '__main__':
    main()
from QuickProject.Commander import Commander
from . import *

app = Commander()


@app.custom_complete('name')
def hello():
    return ['Alice', "Bob", "Candy", "David"]
    # 或者
    return [
        {
            'name': 'Alice',
            'description': 'Alice',
            'icon': '👩'
        },
        {
            'name': 'Bob',
            'description': 'Bob',
            'icon': '👨'
        },
        {
            'name': 'Candy',
            'description': 'Candy',
            'icon': '👧'
        },
        {
            'name': 'David',
            'description': 'David',
            'icon': '👦'
        },
    ]


@app.command()
def hello(name: str):
    """
    echo Hello <name>

    :param name: str
    """
    print(f"Hello {name}!")


def main():
    """
    注册为全局命令时, 默认采用main函数作为命令入口, 请勿将此函数用作它途.
    When registering as a global command, default to main function as the command entry, do not use it as another way.
    """
    app()


if __name__ == '__main__':
    main()

您的 APP 补全选项可能是动态的,QuickProject 暂时还无法完全支持动态补全,然而如果您的补全选项并不经常变更,可以通过 Commander 对象创建时被注册的 complete 隐藏子命令生成补全脚本并选择应用至 fig。

执行<你的命令> complete后,将会在命令运行的目录下生成一个 complete 文件夹,可自动应用 fig 的补全脚本,zsh 补全脚本需要自行拷贝至相应位置。

如果想要自定义补全子命令或不使用子命令,只需在创建 Commander 时:custom_complete=True即可(此时你可以自行决定是否创建 complete 子函数,以及 complete 子函数的实现方式);如果想要隐藏子命令不出现在补全脚本中,只需在创建 Commander 时:non_complete=True即可(此时隐藏子命令将不会被写入补全脚本中)。

python
...

app = Commander(name, custom_complete=True) # 此时,complete函数将不会被自动注册

"""
如果希望隐藏子命令不在补全脚本中出现可进行如下设置
"""
app = Commander(name, non_complete=True) # 此时,不会为隐藏子命令生成补全脚本

...

if __name__ == "__main__":
    app.generate_complete(...) # 允许在代码中调用的补全脚本生成API:将生成complete文件夹,内置fig和zsh补全脚本。
...

app = Commander(name, custom_complete=True) # 此时,complete函数将不会被自动注册

"""
如果希望隐藏子命令不在补全脚本中出现可进行如下设置
"""
app = Commander(name, non_complete=True) # 此时,不会为隐藏子命令生成补全脚本

...

if __name__ == "__main__":
    app.generate_complete(...) # 允许在代码中调用的补全脚本生成API:将生成complete文件夹,内置fig和zsh补全脚本。

WARNING

QuickProject 暂时只支持 zsh 和 fig 的补全脚本生成(其他的我也不会写)。

私有参数

如果希望子命令中的某些参数是不可见的(不会被解析,也不会被帮助菜单展示),可以让参数名以_开头,Commander 将自动跳过此参数的解析,但你可以在代码内部的app.real_call中对私有参数赋值。

WARNING

私有参数必须以可选参数方式定义!

样例:

python
@app.command()
def hello(who: str, _where: bool = False):
    if _where:
        print("Hello!")
    else:
        print(f"Hello {who}")

app.real_call('hello', 'RhythmLian', True) # Hello!
app.real_call('hello', 'RhythmLian')       # Hello RhythmLian
@app.command()
def hello(who: str, _where: bool = False):
    if _where:
        print("Hello!")
    else:
        print(f"Hello {who}")

app.real_call('hello', 'RhythmLian', True) # Hello!
app.real_call('hello', 'RhythmLian')       # Hello RhythmLian

Released under the MIT License.