Тест-драйв Solara для разработки веб-приложений на Python

Тест-драйв Solara для разработки веб-приложений на Python

https://t.me/ai_machinelearning_big_data

Введение 

Как вам идея создавать веб-приложения полностью на Python? Звучит заманчиво! Разработка веб-приложения требует владения навыками как фронтенда, так и бэкенда. В их число входят HTML, CSS, JavaScript, разные фреймворки, а также Python и другие серверные языки бэкенда. Как видно, предполагается большой объем работы! 

А если бы для создания веб-приложения использовался только Python, теоретически даже один файл, то скорость разработки возросла бы в разы. В настоящее время существуют несколько библиотек, которые пытаются реализовать эту идею на практике. Самая популярная из них, судя по количеству звезд на GitHub, — это Streamlit. На момент написания статьи она была отмечена 24,9 тыс. звезд. 

Несмотря на невероятную лаконичность Streamlit и способность создавать веб-приложения в несколько строк кода, у этой библиотеки есть и недостатки. Один из них — ограниченные возможности для создания пользовательских интерфейсов. Кроме того, следует отметить ее довольно странный механизм, при котором все перезапускается при каждом изменении состояния. 

Не имею ничего против Streamlit. С удовольствием с ней работаю, если она способствует решению поставленных задач. Но вот для создания приложений со сложными интерфейсами и/или вложенными состояниями она не подходит. 

Таким образом, для воплощения задуманного нужен новый, более настраиваемый фреймворк, который органично бы объединял гибкость фронтенд-фреймворков и Python. 

Solara

Не так давно был представлен фреймворк Solara, предназначенный для создания веб-приложений на чистом Python. В его документации содержатся интересные улучшения Streamlit. К ним относятся вложенные переиспользуемые компоненты с собственными состояниями, не требующие повторного выполнения без необходимости, а также простая интеграция с Jupyter Notebook.

Особенно впечатляет тот факт, что официальный сайт Solara также создан с помощью фреймворка Solara, чего не скажешь о Streamlit. Предлагаем ссылки на официальный репозиторий GitHub и сайт Solara

В теории все звучит прекрасно. Но как выглядит код? Так ли он хорош на практике? Помогает ли он эффективно создавать нужные приложения? 

В следующих разделах статьи мы протестируем данный фреймворк. Для этого создадим что-нибудь одновременно простое, но при этом достаточно сложное, чтобы проверить его возможности, а именно приложение-планировщик задач todo app. Я уже проводил подобный эксперимент с Shiny для Python, теперь настала очередь Solara. Изображение итогового результата: 

В конце статьи продемонстрируем работу получившегося приложения. За дело! 

Написание кода в Solara

Перед началом работы следует отметить пару моментов. Во-первых, фреймворк является совершенно новым, и сложно предсказать возможные изменения по мере его развития. Кроме того, я не претендую на звание эксперта Solara. 

Начнем с определения глобальных переменных: 

import solara

text_input = solara.reactive("")
todos = solara.reactive([
    { "text": solara.reactive("Learn Solara"), "done": solara.reactive(False) },
    { "text": solara.reactive("Build a Solara app"), "done": solara.reactive(False) }
])

Как видно, эта операция выполняется с помощью solara.reactive(...). Для строковых переменных просто добавляется строка в качестве начального значения. Для приложения-планировщика срабатывает добавление вложенных элементов словаря, также определенных с помощью solara.reactive

Page()

Затем определяем основной компонент с именем Page:

@solara.component
def Page():
    # добавление css
    solara.Style("""
        .add-button {
            margin-right: 10px;
        }
    """)

    # центрирование карты
    with solara.Column(align="center"):
        with solara.Card(title="Todo App"):
            for todo in todos.value:
                Todo(todo)
            if len(todos.value) == 0:
                solara.Text("No todos yet.")
            
            solara.InputText(label="Add a todo", value=text_input),
            solara.Button("Add", on_click=on_add_todo, classes=["primary", "add-button"]),
            solara.Button("Remove finished tasks", classes=["secondary"], on_click=clear_finished_todos),

Page()

Рассмотрим код шаг за шагом. Компонент определяется посредством декоратора @solara.component. В данном случае это основной компонент, содержащий весь интерфейс. Сначала в компоненте вызывается solara.Style(...) для добавления стилей CSS к одной из кнопок (обратите внимание на атрибуты classes=[..., “add-button”] для этой кнопки). Не стоит волноваться, если вы не знакомы с CSS. Данная операция относится к разряду нетипичных, о чем и предупреждается в документации.

ПРИМЕЧАНИЕ. ЭТО ОПЕРАЦИЯ СЧИТАЕТСЯ ФУНКЦИОНАЛЬНОСТЬЮ ПОВЫШЕННОГО УРОВНЯ СЛОЖНОСТИ, ТРЕБУЮЩЕЙ ОСТОРОЖНОГО ПОДХОДА. 

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

Далее создаем solara.Column для центрирования компонентов внутри и solara.Card для создания стилизованного контейнера: 

with solara.Column(align="center"):
    with solara.Card(title="Todo App"):
        ...

На следующем этапе перебираем элементы в глобальном состоянии todos, которое было определено ранее, и передаем каждое состояние в списке отдельному компоненту Todo:

for todo in todos.value:
    Todo(todo)
if len(todos.value) == 0:
    solara.Text("No todos yet.")

Код для компонента Todo рассмотрим позже. 

Наконец, код предусматривает текстовое поле для ввода задач и 2 кнопки: одна для добавления новых задач, а другая для удаления выполненных.

solara.InputText(label="Add a todo", value=text_input),
solara.Button("Add", on_click=on_add_todo, classes=["primary", "add-button"]),
solara.Button("Remove finished tasks", classes=["secondary"], on_click=clear_finished_todos),

Для текстового поля значение устанавливается в одно из глобальных состояний, которое автоматически обновляется при потери элементом фокуса (или при каждом нажатии клавиши совместно с continuous_update=True).

Обе кнопки располагают обратными вызовами on_click для обработки логики при нажатии. Рассмотрим код: 

def on_add_todo():
    todos.set(todos.value + [{
        "text": solara.Reactive(text_input.value),
        "done": solara.Reactive(False)
    }])
    text_input.set("")

def clear_finished_todos():
    todos.set([todo for todo in todos.value if not todo["done"].value])

Первый метод on_add_todo выполняет конкатенацию старых элементов todo с новым элементом, который принимает значение внутри текстового поля, а затем его очищает. Обратите внимание, что переменные todos и text_input являются объектами Reactive (определенными ранее с помощью solara.Reactive(...)), которые обрабатывают логику, необходимую для состояний. Таким образом, для доступа к их текущим фактическим значениям требуется задействовать метод доступа .value

Второй метод clear_finished_todos перебирает элементы todo и удаляет те, которые выполнены, т.е. “done”. Поскольку элемент словаря done также является объектом Reactive, доступ к его значению осуществляется посредством .value.

Todo()

Последний фрагмент мозаики — функция Todo, переиспользуемый компонент, с помощью которого отображается каждый элемент todo. Ниже представлен код: 

@solara.component
def Todo(todo):
    # определение локального состояния, только для данного компонента
    editing, set_editing = solara.use_state(False)

    # размер 0 будет занимать минимум места 
    with solara.Columns([1, 0]):
        # установка фонового цвета в зависимости от состояния done 
        color = "#d6ffd6" if todo["done"].value else "initial"
        # css для придания привлекательного внешнего вида
        with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
            # если редактирование (editing) является true, элемент редактируется 
            if editing:
                solara.InputText(label="Edit todo", value=todo["text"])
            else:
                solara.Checkbox(label=todo["text"].value, value=todo["done"])

        # кнопки для редактирования/сохранения и удаления 
        solara.Column(children=[
            (
                solara.IconButton(icon_name="save", on_click=lambda: set_editing(False))
                if editing 
                else 
                solara.IconButton(icon_name="edit", on_click=lambda: set_editing(True))
            ),
            solara.IconButton(icon_name="delete", on_click=lambda: todos.set([t for t in todos.value if t != todo]))
        ])

Сначала вызывается solara.use_state(<initial value>) для определения локального состояния внутри компонента. Если вы работали с функциональными компонентами в ReactJS, то эта процедура вам знакома. 

Далее определяем 2 столбца:

with solara.Columns([1, 0]):
    ...

Один из них должен растягиваться (текст), а другой — быть минимального размера (кнопки). 

Текст отображается следующим образом:

# установка фонового цвета в зависимости от состояния done
color = "#d6ffd6" if todo["done"].value else "initial"
# css для придания привлекательного внешнего вида
with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
    # если редактирование (editing) является true, элемент редактируется
    if editing:
        solara.InputText(label="Edit todo", value=todo["text"])
    else:
        solara.Checkbox(label=todo["text"].value, value=todo["done"])

Добавляем стиль CSS background-color в зависимости от состояния done вместе с фиксированными стилями CSS. Затем с учетом состояния editing содержимое подгоняется либо под флажок (checkbox), либо поле для ввода текста. Флажок, по аналогии с текстовым полем, обновляет value объекта Reactive при нажатии.

И наконец, обзаводимся кнопками в виде иконок, расположенными вертикально: 

# кнопки для редактирования/сохранения и удаления
solara.Column(children=[
    (
        solara.IconButton(icon_name="save", on_click=lambda: set_editing(False))
        if editing 
        else 
        solara.IconButton(icon_name="edit", on_click=lambda: set_editing(True))
    ),
    solara.IconButton(icon_name="delete", on_click=lambda: todos.set([t for t in todos.value if t != todo]))
])

При редактировании отображается кнопка save, в противном случае — кнопка edit. Кроме того, имеется кнопка delete для удаления текущих задач. 

Готово! Посмотрим, что получилось: 

Обратите внимание, что при нажатии на флажок в компоненте Todo, обновляется только компонент Todo, а не компонент Page. Такой подход позволяет создавать более эффективные программы. 

Полный вариант кода:

import solara

text_input = solara.reactive("")
todos = solara.reactive([
    { "text": solara.reactive("Learn Solara"), "done": solara.reactive(False) },
    { "text": solara.reactive("Build a Solara app"), "done": solara.reactive(False) }
])

def on_add_todo():
    todos.set(todos.value + [{
        "text": solara.Reactive(text_input.value),
        "done": solara.Reactive(False)
    }])
    text_input.set("")

def clear_finished_todos():
    todos.set([todo for todo in todos.value if not todo["done"].value])

@solara.component
def Todo(todo):
    # определение локального состояния только для данного компонента 
    editing, set_editing = solara.use_state(False)

    # размер 0 будет занимать минимум места 
    with solara.Columns([1, 0]):
        # установка фонового цвета в зависимости от состояния done 
        color = "#d6ffd6" if todo["done"].value else "initial"
        # css для придания привлекательного внешнего вида 
        with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
            # если редактирование является true, элемент редактируется 
            if editing:
                solara.InputText(label="Edit todo", value=todo["text"])
            else:
                solara.Checkbox(label=todo["text"].value, value=todo["done"])

        # кнопки для редактирования/сохранения и удаления
        solara.Column(children=[
            (
                solara.IconButton(icon_name="save", on_click=lambda: set_editing(False))
                if editing 
                else 
                solara.IconButton(icon_name="edit", on_click=lambda: set_editing(True))
            ),
            solara.IconButton(icon_name="delete", on_click=lambda: todos.set([t for t in todos.value if t != todo]))
        ])

@solara.component
def Page():
    solara.Style("""
        .add-button {
            margin-right: 10px;
        }
    """)

    # to центрирование карты  
    with solara.Column(align="center"):
        with solara.Card(title="Todo App"):
            for todo in todos.value:
                Todo(todo)
            if len(todos.value) == 0:
                solara.Text("No todos yet.")
            
            solara.InputText(label="Add a todo", value=text_input),
            solara.Button("Add", on_click=on_add_todo, classes=["primary", "add-button"]),
            solara.Button("Remove finished tasks", classes=["secondary"], on_click=clear_finished_todos),

Page()

Запуск приложения Solara в Jupyter Notebook

Этот код можно легко запустить в Jupyter Notebook. Для этого просто вставляем код в ячейку и запускаем его: 

Заключение 

Тестирование фреймворка Solara позволило обнаружить в нем ряд преимуществ, отсутствующих в Streamlit. Среди них — переиспользуемые компоненты, усложненная функциональность с локальными и глобальными состояниями, а также более широкие возможности настройки, например с CSS. 

Однако выявились и недочеты. Во-первых, хотелось бы задействовать базовые HTML-компоненты. На данный момент можно вызвать solara.HTML(...), но только для изолированного рендеринга HTML с единственной возможностью чтения. Обернуть в них другие компоненты уже не получится. Помимо этого, отсутствуют и другие базовые компоненты. Например, нет многострочных текстовых полей, хотя вероятно их появление в ближайшем будущем. 

В целом, фреймворк Solara обладает большим потенциалом, и в моей практике он придет на смену Streamlit. Но в настоящее время он не готов заменить ReactJS на стороне клиента и Python на стороне сервера в ситуациях, требующих исключительной гибкости. 


Источник

Report Page