Для описания объектов и процессов в терминах бизнес-логики, конфигурирования и определения структуры и логики в сложных системах популярным подходом является использование предметно-специфических языков (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, а также рассмотрим пример небольшого приложения, осветим особенности развертывания эксплуатации.
