它是什么
PyMini 是用 Swift 写的 Python 子集解释器,就装在键盘里。你写的每个面板、每个动作都跑在它上面。它读的是你已经熟悉的那种 Python,画界面的脚本和改写单词的脚本用的是同一门语言。
# an action: text in, text out
def transform(text):
return text.upper()
# a panel: a view, and what a tap does
def view(state):
return button("Shout", "shout")
def on_action(action, state):
insert(context()["word"].upper())
return state为什么用它
我们做 PyMini,是因为 Clink 需要一种让人用脚本扩展自己键盘的方式,而我们找到的东西没有一个能塞进键盘里。不过它本身和键盘毫无绑定,所以现在它作为独立的 Swift 包开源了,任何想让用户写点 Python 的 app 都能用。
它不是 CPython,也没有把 CPython 装进来。键盘扩展拿到的内存只有 app 的一小部分,光是 CPython 的二进制和堆就会在你的脚本跑起来之前把这点预算花光。PyMini 只是几百 KB 的 Swift,没有任何二进制依赖。
凡是用户想要自己一套逻辑的地方都用得上:写作 app 里的文本转换、待办或记账 app 里的规则、公式、自动化、游戏 mod,或者给有社区的 app 做插件系统。很多人本来就会一点 Python,不用再教一门新语言,而脚本就是纯文本,可以像其他设置一样保存、同步和分享。
import PyMini
let py = PyRuntime(modules: ["math", "json", "re"])
// your app, as a Python function
py.define("shout") { call in
try call.argument(0, as: String.self).uppercased()
}
// a script one of your users wrote
try py.load("""
def tidy(text):
return shout(" ".join(text.split()))
""")
try py.call("tidy", " hello there ") // HELLO THERE| 调用 | 作用 |
|---|---|
import PyMini | 没有依赖,也没有要手动打包的东西。加上这个 Swift 包,就能在 iOS 15、macOS 12、tvOS 15、watchOS 8 和 visionOS 1 及更高版本上运行。 |
py.define("shout") { call in ... } | 你 app 的功能会变成 Python 函数和模块。参数以 Swift 类型传进来,你返回的东西会以 Python 值回到脚本里。 |
PyLimits(steps: 200_000) | 每次调用都在步数预算内运行,预算用完就是一个任何脚本都抓不住的错误。随手写的 while True 会以一条消息结束,而不是让 app 卡死。 |
PyRuntime(modules: ["math"]) | 脚本碰不到文件、网络和进程。它们只能用到你定义的函数,以及你保留开启的标准模块。 |
py.call(hook: "on_open") | app 里发生什么事时就调用脚本。脚本没定义的钩子会直接跳过,所以每个钩子都可以是可选的。 |
catch let error as PyError | 错误会带着 Python 异常名和行号返回,可以直接给写脚本的人看。 |
import sys | sys.version 会给出解释器的名字和版本,pymini 命令也打印同一行。脚本因此能报出运行它的 app 里装的是哪个版本。 |
源代码、语言参考和测试都在 GitHub 上。
它承诺什么
脚本够不到文件系统、网络,也够不到任何进程:沙箱是解释器本身的形状,而不是一份拒绝清单。通向外面的门只有 import 一扇,门后是少数几个只做计算的命名空间。其余一切都要花步数。每个表达式、循环的每一圈、正则的每一步、JSON 的每一行,都从 2,000,000 里花掉一步,字符串和列表也在增长时受上限约束。
共享面板中可导入的命名空间
jsonmathrandomresystime
这份预算抓不住。except 子句能抓到 KeyError,却抓不到终止失控循环的那次中断,因为抓住它,正是失控脚本得以继续跑下去的办法。
更新日志
PyMini 的每一版,最新的在前,并列出各版新增的内容。
PyMini 2.0
第一版能把一个词变成大写,能摆出一格一格的按钮。第二版是一门可以写小程序的语言,是一块会回应点按、而不只是带着固定反应的面板。
| 示例 | 变更内容 |
|---|---|
[word for word in words if word] | 推导式、lambda、元组、集合,以及 try / except / finally / raise。 |
f"{total:,.2f}" | f-string 的转换和完整的格式说明,与 str.format、format() 共用一套。 |
f"""Hello {name}""" | 三引号字符串、原始字符串,以及可以跨多行的 f-string。 |
import math, random, time, json, re | 命名空间只有五个,别无其他,而且个个都是纯的。 |
re.sub(r"\s+", " ", text) | 正则跑在 Clink 自己的引擎上,花的是同一份步数预算。 |
on_action(action, value, state) | 面板自己处理点按,保有真正的状态,还多了十一种控件。 |
replace(len(word), word.upper()) | 脚本可以请键盘动手,也可以读到光标周围的内容。 |
Current sentence | 动作可以读输入框里的任意一段,不再只有当前词和它前面的内容。 |
import re
def initial():
return {"words": []}
def view(state):
top = sorted(set(state["words"]), key=lambda w: -len(w))[:3]
return vstack([text(f"{len(state['words']):,} words", mono=True)]
+ [row(w, detail=f"{len(w)}") for w in top])
def on_open(state):
state["words"] = re.findall(r"\w+", context()["before"])
return statePyMini 1.0
第一版。动作把一个字符串变成另一个字符串,面板只是几个控件,按钮要么输入固定的文字,要么设置一个值。
| 示例 | 变更内容 |
|---|---|
def transform(text): | int、float、str、bool、None、list 和 dict,以及 if、while、for,还有支持默认参数和关键字参数的 def。 |
f"{n} words" | 不带格式说明的 f-string,以及支持负索引的索引和切片。 |
sorted(text.split()) | 24 个内置函数,以及 str、list、dict 的常用方法。 |
button("+", set={"count": 1}) | 文本、按钮、输入框、堆叠和网格。按钮只能输入文字或设置状态,仅此而已。 |
while True: | 完全没有 import,每个脚本都有步数预算,字符串和列表能长到多大也有上限。 |
接下来看什么
完整的语言面貌、面板的词汇,以及一个你一行都不用打、两样都替你写好的构建器。
操作 ›面板 ›插件 ›创作器 ›