Что это
PyMini это интерпретатор подмножества Python, написанный на Swift и встроенный в клавиатуру. На нём работают все ваши панели и все ваши действия. Он читает тот 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.
Это не CPython, и он его не встраивает. Расширению клавиатуры достаётся доля той памяти, что получает приложение, и двоичный файл с кучей CPython израсходовали бы этот бюджет раньше, чем запустился бы ваш скрипт. PyMini это пара сотен килобайт Swift без единой двоичной зависимости.
Он пригодится везде, где пользователям нужна своя логика: преобразования текста в приложении для письма, правила в списке дел или приложении для бюджета, формулы, автоматизация, моды для игр или система плагинов для приложения с сообществом. Многие уже немного знают 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 ... } | Возможности вашего приложения становятся функциями и модулями Python. Аргументы приходят как типы Swift, а то, что вы вернёте, попадает обратно в скрипт как значение Python. |
PyLimits(steps: 200_000) | Каждый вызов работает в пределах бюджета шагов, и его исчерпание это ошибка, которую не перехватит ни один скрипт. Неосторожный while True заканчивается сообщением, а не зависшим приложением. |
PyRuntime(modules: ["math"]) | У скриптов нет файлов, сети и процессов. Им доступны только функции, которые вы определили, и стандартные модули, которые вы оставили включёнными. |
py.call(hook: "on_open") | Вызывайте скрипт, когда в приложении что-то происходит. Хук, который скрипт не определил, просто пропускается, так что любой хук может быть необязательным. |
catch let error as PyError | Ошибки возвращаются с именем исключения Python и номером строки, так что их можно сразу показать автору скрипта. |
import sys | sys.version называет интерпретатор и версию, а команда pymini печатает ту же строку. Так скрипт может сообщить, какая сборка внутри приложения, которое его запустило. |
Исходный код, описание языка и тесты лежат на 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-строк и полные спецификации формата, общие с str.format и format(). |
f"""Hello {name}""" | Строки в тройных кавычках, сырые строки и многострочные f-строки. |
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-строки без спецификаций формата, индексация и срезы с отрицательными индексами. |
sorted(text.split()) | 24 встроенные функции и привычные методы str, list и dict. |
button("+", set={"count": 1}) | Текст, кнопки, поля, стеки и сетка. Кнопка могла ввести текст или изменить состояние, не больше. |
while True: | Никаких import, бюджет шагов для каждого скрипта и ограничения на то, насколько могут вырасти строки и списки. |
Куда дальше
Язык целиком, словарь панелей и конструктор, который пишет и то и другое, пока вы не набрали ни строчки.
Действия ›Панели ›Плагины ›Конструктор ›