框架工具
正如前述的 Commander 样例中,一个 Commander APP 需要被设定一个入口,以便于它被注册至全局命令亦或者方便从 Pypi 安装。
因此,一个经典的 Commander APP 的目录结构为(以 test 为例):
📂 .
├── 📒 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
它的配置表内容为
{
"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 在本地存储配置信息的部分。它的内容为:
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)
从引入相关包以后,可以选择:
- 项目是否启用配置文件(默认为不启用)
- 设置配置文件存储位置(默认为用户根目录下的
.<项目名>_config
,并以 json 格式存储) - 自定义问题。
自定义问题
如上述代码所见,在questions
字典中加入你需要在配置表存储的键值对,即可添加问题。
问题的类型可以参照此问题样例来添加,常见的问题种类包括input
(用户自行输入一串文本)、list
(单选)、checkbox
(多选)、confirm
(是或否)。
test/__init__.py
此文件提供若干基础 API,比如引用依赖
的询问安装,执行命令
等,如果启用了配置表,则可全局使用config
对象来获取配置表信息。可自定义此文件内容,以便->from . import balabala
。
动态引用
QuickProject.Commander 提供动态引入功能,下面将通过使用样例来展示它的功能。
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 的入口文件,它的结构如下
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 类型来表示是否验证成功。
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
菜单里占据位置,因此我们可以暂时隐藏它:
@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 某个子命令的参数可能是几个固定值中的一个,您可能希望在补全时直接提示这几个候选项。
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
即可(此时隐藏子命令将不会被写入补全脚本中)。
...
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
私有参数必须以可选参数方式定义!
样例:
@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