Что это

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
Один язык, два контракта: transform(text) для действия, view(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
Передайте скриптам функцию на Swift, загрузите скрипт и вызовите его.
ВызовЧто делает
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 syssys.version называет интерпретатор и версию, а команда pymini печатает ту же строку. Так скрипт может сообщить, какая сборка внутри приложения, которое его запустило.

Исходный код, описание языка и тесты лежат на 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-строк и полные спецификации формата, общие с 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 state
Панель, которая читает написанное, считает его и перечисляет самые длинные слова.

PyMini 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, бюджет шагов для каждого скрипта и ограничения на то, насколько могут вырасти строки и списки.

Куда дальше

Язык целиком, словарь панелей и конструктор, который пишет и то и другое, пока вы не набрали ни строчки.

Действия ›Панели ›Плагины ›Конструктор ›
Загрузить в App Store