Для описания объектов и процессов в терминах бизнес-логики, конфигурирования и определения структуры и логики в сложных системах популярным подходом является использование предметно-специфических языков (Domain Specific Language - DSL), которые реализуются либо через синтаксические особенности языка программирования (например, с использованием средств метапрограммирования, аннотаций/декораторов, переопределения операторов и создания инфиксных операторов, как например в Kotlin DSL) или с помощью применения специализированных инструментов разработки и компиляторов (например, Jetbrains MPS или парсеров общего назначения, таких как ANTLR или Bison). Но существует также подход реализации DSL, основанный на синтаксическом разборе и одновременной кодогенерации для создания исполняемого кода по описанию и в этой статье мы рассмотрим некоторые примеры использования библиотеки textx для создания DSL на Python.
textX - это инструмент для создания языковых моделей (DSL) на Python. Он позволяет быстро и легко определить грамматику языка и сгенерировать парсер для этого языка. textX распространяется с открытым исходным кодом, легко интегрируется с другими инструментами Python и может быть использован в различных проектах, где необходимо определять и обрабатывать языки на основе текста.
С помощью textX можно определять различные типы языковых конструкций, такие как ключевые слова, идентификаторы, числа, строки и т. д., а также определять их свойства, например, типы данных или ограничения на значения. Определение грамматики языка происходит в текстовом файле в формате, который называется meta-DSL (и зарегистрован как язык tx, связанный с маской файлов *.tx). Метамодель используется для проверки синтаксической корректности DSL-модели и позволяет сформировать дерево объектов для использования в кодогенераторе Python (который может сформировать как другой Python-код, так и исходный текст на любом другом языке программирования и даже сделать PDF-документ или создать dot-файл для graphviz с целью визуализации мета-модели).
textX позволяет создавать простые и сложные DSL, например, для описания конфигурационных файлов, для создания доменно-специфических языков (DSL), используемых в различных отраслях, таких как бизнес-правила, научные вычисления, обработка естественного языка и многие другие.
Для использования textX прежде всего установим необходимые модули:
pip install textX click
После установки появится консольная утилита textx, которая будет использоваться для проверки корректности метамоделей и DSL-модулей (в соответствии с грамматикой языка). Textx использует setuptools для поиска зарегистрированных компонентов и позволяет расширять свои возможности через добавление языков (список может быть просмотрен через textx list-languages
) и подключение генераторов (textx list-generators
). Расширения могут быть установлены как модули через pip install, например:
textx-jinja - использование шаблонизатора jinja для преобразования DSL-модели в текстовый документ (например, в HTML)
textx-lang-questionnaire - DSL для определения опросников
PDF-Generator-with-TextX - генератор PDF на основе DSL-описания
Мы будем создавать свой язык без использования дополнительных расширений, чтобы увидеть весь процесс. Начнем с простой задачи интерпретатора текстового файла, состоящего из строк "hello ", который будет генерировать выполняемый код на Python для отображения адресных приветствий. Генератор и описание языка будем создавать в пакете hello и также определим файл setup.py для конфигурирования точек входа в извлечение метамодели для определяемого языка и генератора кода.
Описание (hello.tx) может выглядеть следующим образом:
DSL:
hello*=Hello
;
Hello:
'Hello' Name
;
Name:
name=/[A-Za-z\ 0-9]+/
;
В определении используются условные обозначения:
hello*=Hello
- перечисление из нескольких элементов (Hello), могут отсутствовать (будут собираться в список hello)hello+=Hello
- один или несколько элементовhello?=Hello
- элемент может присутствовать, но необязательно (в модели будет None)hello=Hello
- точно один элемент
Также определение может включать в себя строковые константы, регулярные выражения, их группировки (например, через вертикальную черту обозначается выбор одного из значений) с модификаторами (+
, ?
, *
имеют обычный смысл как для регулярных выражений, # подразумевает возможный произвольный порядок определений).
Создадим определение языка в hello/__init__.py
:
import os.path
from os.path import dirname
from textx import language, metamodel_from_file
@language('hello', '*.hello')
def hello():
"""Sample hello language"""
return metamodel_from_file(os.path.join(dirname(__file__), 'hello.tx'))
Здесь мы определяем новый язык с идентификатором hello
, который будет применяться к любым файлам с маской имени *.hello
. Добавим определение setup.py
:
from setuptools import setup, find_packages
setup(
name='hello',
packages=find_packages(),
package_data={"": ["*.tx"]},
version='0.0.1',
install_requires=["textx_ls_core"],
description='Hello language',
entry_points={
'textx_languages': [
'hello = hello:hello'
]
}
)
И установим наш модуль:
python setup.py install
И проверим наличие установленного языка:
textx list-languages
textX (*.tx) textX[3.1.1] A meta-language for language definition
hello (*.hello) hello[0.0.1] Sample hello language
Теперь создадим тестовую модель, основанную на грамматике (test.hello):
Hello World
Hello Universe
Проверим корректность метамодели и нашей DSL-модели (на соответствие метамодели):
textx check hello/hello.tx
hello/hello.tx: OK.
textx check test.hello
test.hello: OK.
Мы можем получить визуальную диаграмму для описания композиции DSL (может быть создано описание для Graphviz или для PlantUML):
textx generate hello/hello.tx --target dot
dot -Tpng -O hello/hello.dot
Теперь добавим возможность генерации кода, но перед этим сделаем программный разбор DSL-определения на метамодели:
from textx import metamodel_from_file
metamodel = metamodel_from_file('hello/hello.tx')
dsl = metamodel.model_from_file('test.hello')
for entry in dsl.hello:
print(entry.name)
Здесь мы увидим список имен (World, Universe), которые относятся к термам Hello (сами термы будут доступны через список hello в корневом объекте разобранной модели). Добавим теперь возможность кодогенерации, для этого добавим функцию с аннотацией @generator:
@generator('hello', 'python')
def python(metamodel, model, output_path, overwrite, debug, **custom):
"""Generate python code"""
if output_path is None:
output_path = dirname(__file__)
with open(output_path + "/hello.py", "wt") as p:
for entry in model.hello:
p.write(f"print('Generated Hello {entry.name}')\n")
print(f"Generated file: {output_path}/hello.py")
Генератор создается для языка hello и будет доступен как target python. Добавим в entry_points регистрацию в setup.py:
'textx_generators': [
'python = hello:python'
]
И теперь выполним генерацию кода:
textx generate test.hello --target python
Результат генерации будет выглядеть так:
print('Generated Hello World')
print('Generated Hello Universe')
Теперь немного усложним задачу и реализуем конечный автомат. Определение DSL для конечного автомата может выглядеть так:
states {
RED,
YELLOW,
RED_WITH_YELLOW,
GREEN,
BLINKING_GREEN
}
transitions {
RED -> RED_WITH_YELLOW (on1)
RED_WITH_YELLOW -> GREEN (on2)
GREEN -> BLINKING_GREEN (off1)
BLINKING_GREEN -> YELLOW (off2)
YELLOW -> RED (off3)
}
Здесь мы перечисляем возможные состояния и переходы между ними (с идентификаторами переходов). Для определения грамматики можно использовать такое описание:
StateMachine:
'states' '{'
states+=State
'}'
'transitions' '{'
transitions+=Transition
'}'
;
State:
id=ID (',')?
;
TransitionName:
/[A-Za-z0-9]+/
;
Transition:
from=ID '->' to=ID '(' name=TransitionName ')'
;
Здесь используется модификатор необязательности для запятой после идентификатора события. Созданную модель можно использовать непосредственно, через применение метамодели DSL к описанию состояний светофора:
from textx import metamodel_from_file
metamodel = metamodel_from_file('statemachine/statemachine.tx')
dsl = metamodel.model_from_file('test.sm')
# описание перехода
class Transition:
state_from: str
state_to: str
action: str
def __init__(self, state_from, state_to, action):
self.state_from = state_from
self.state_to = state_to
self.action = action
def __repr__(self):
return f"{self.state_from} -> {self.state_to} on {self.action}"
states = map(lambda state: state.id, dsl.states)
transitions = map(lambda transition: Transition(transition.__getattribute__('from'), transition.to, transition.name),
dsl.transitions)
# извлекаем название действий (для меню)
actions = list(map(lambda t: t.action, transitions))
И теперь реализуем конечный автомат:
current_state = states[0]
while True:
print(f'Current state is {current_state}')
print('0. Exit')
for p, a in enumerate(actions):
print(f'{p + 1}. {a}')
i = int(input('Select option: '))
if i == 0:
break
if 0 < i <= len(actions):
a = actions[i-1]
for t in transitions:
if t.state_from == current_state and t.action == a:
current_state = t.state_to
break
Альтернативным решением может быть генерация функции перехода на основе DSL, которая будет принимать enum-значение из пространства состояний и переход (из списка возможных) и возвращает новое состояние или исключение, если такой переход не реализован.
Аналогично может быть реализовано более сложная грамматика как для представления данных, так и для описания алгоритмических конструкций языка (таких как ветвления или циклы).
В конце статьи хочу порекомендовать бесплатный урок, на котором обсудим основы разработки API с помощью фреймворка FastAPI, а также рассмотрим пример небольшого приложения, осветим особенности развертывания эксплуатации.