它是什么

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
一门语言,两份约定:动作用 transform(text),面板用 view(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
给脚本一个 Swift 函数,加载脚本,然后调用它。
调用作用
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 syssys.version 会给出解释器的名字和版本,pymini 命令也打印同一行。脚本因此能报出运行它的 app 里装的是哪个版本。

源代码、语言参考和测试都在 GitHub 上。

它承诺什么

脚本够不到文件系统、网络,也够不到任何进程:沙箱是解释器本身的形状,而不是一份拒绝清单。通向外面的门只有 import 一扇,门后是少数几个只做计算的命名空间。其余一切都要花步数。每个表达式、循环的每一圈、正则的每一步、JSON 的每一行,都从 2,000,000 里花掉一步,字符串和列表也在增长时受上限约束。

共享面板中可导入的命名空间

  • json
  • math
  • random
  • re
  • sys
  • time

这份预算抓不住。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 state
一块读你写过的内容、数一数,再把最长的词列出来的面板。

PyMini 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,每个脚本都有步数预算,字符串和列表能长到多大也有上限。

接下来看什么

完整的语言面貌、面板的词汇,以及一个你一行都不用打、两样都替你写好的构建器。

操作 ›面板 ›插件 ›创作器 ›
在 App Store 下载