Исходный код : https://github.com/volfpeter/htmy
Документация и примеры : https://volfpeter.github.io/htmy
htmyАсинхрон , чистопитонный двигатель рендеринга.
ErrorBoundary компонент для изящной обработки ошибок.Пакет доступен на PYPI и может быть установлен с:
$ pip install htmy Вся библиотека-от самого двигателя рендеринга до встроенных компонентов-построена вокруг нескольких простых протоколов и нескольких простых классов полезности. Это означает, что вы можете легко настроить, расширить или заменить в основном все в библиотеке. Да, даже двигатель рендеринга. Оставшиеся детали будут продолжать работать, как и ожидалось.
Кроме того, библиотека не полагается на усовершенствованные функции Python, такие как Metaclasses или дескрипторы. Также нет сложных базовых классов и тому подобного. Даже младший инженер мог понимать, разрабатывать и отлаживать приложение, созданное с htmy .
Каждый класс с синхронизацией или асинхронной htmy(context: Context) -> Component метод является компонентом htmy (технически HTMYComponentType ). Строки также являются компонентами, а также списки или кортежи HTMYComponentType или строковых объектов.
Использование этого названия метода позволяет превращать любой из ваших бизнес -объектов (из TypedDicts S или pydantic Models в классы ORM) в компоненты без страха перед столкновением имени с другими инструментами.
Async Support позволяет загружать данные или выполнять Async Business Logic прямо в ваших компонентах. Это может уменьшить количество шаблонов, которое вам необходимо писать в некоторых случаях, а также дает вам свободу разделить логику рендеринга и неповдоседения любым способом, который вы считаете нужным.
Пример:
from dataclasses import dataclass
from htmy import Component , Context , html
@ dataclass ( frozen = True , kw_only = True , slots = True )
class User :
username : str
name : str
email : str
async def is_admin ( self ) -> bool :
return False
class UserRow ( User ):
async def htmy ( self , context : Context ) -> Component :
role = "admin" if await self . is_admin () else "restricted"
return html . tr (
html . td ( self . username ),
html . td ( self . name ),
html . td ( html . a ( self . email , href = f"mailto: { self . email } " )),
html . td ( role )
)
@ dataclass ( frozen = True , kw_only = True , slots = True )
class UserRows :
users : list [ User ]
def htmy ( self , context : Context ) -> Component :
# Note that a list is returned here. A list or tuple of `HTMYComponentType | str` objects is also a component.
return [ UserRow ( username = u . username , name = u . name , email = u . email ) for u in self . users ]
user_table = html . table (
UserRows (
users = [
User ( username = "Foo" , name = "Foo" , email = "[email protected]" ),
User ( username = "Bar" , name = "Bar" , email = "[email protected]" ),
]
)
) htmy также предоставляет декоратор @component , который можно использовать для синхронизации или Async my_component(props: MyProps, context: Context) -> Component для преобразования их в компоненты (сохранение печати props ).
Вот тот же пример, что и выше, но с функциональными компонентами:
from dataclasses import dataclass
from htmy import Component , Context , component , html
@ dataclass ( frozen = True , kw_only = True , slots = True )
class User :
username : str
name : str
email : str
async def is_admin ( self ) -> bool :
return False
@ component
async def user_row ( user : User , context : Context ) -> Component :
# The first argument of function components is their "props", the data they need.
# The second argument is the rendering context.
role = "admin" if await user . is_admin () else "restricted"
return html . tr (
html . td ( user . username ),
html . td ( user . name ),
html . td ( html . a ( user . email , href = f"mailto: { user . email } " )),
html . td ( role )
)
@ component
def user_rows ( users : list [ User ], context : Context ) -> Component :
# Nothing to await in this component, so it's sync.
# Note that we only pass the "props" to the user_row() component (well, function component wrapper).
# The context will be passed to the wrapper during rendering.
return [ user_row ( user ) for user in users ]
user_table = html . table (
user_rows (
[
User ( username = "Foo" , name = "Foo" , email = "[email protected]" ),
User ( username = "Bar" , name = "Bar" , email = "[email protected]" ),
]
)
) У htmy есть богатый набор встроенных утилит и компонентов как для HTML, так и для других случаев использования:
html : полный набор базовых тегов HTML.md : MarkdownParser Utility и MD -компонент для загрузки, анализа, преобразования и рендеринга содержимого уценки.i18n : коммунальные услуги для асинхронной, интернационализации на основе JSON.BaseTag , TagWithProps , Tag , WildcardTag : Базовые классы для пользовательских тегов XML.ErrorBoundary , Fragment , SafeStr , WithContext : утилиты для обработки ошибок, обертки компонентов, поставщики контекста и форматирование.Snippet : утилита класса для загрузки и настройки фрагментов документов из файловой системы.etree.ETreeConverter : утилита, которая преобразует XML в дерево компонентов с поддержкой пользовательских компонентов HTMY. htmy.HTMY -это встроенный визуализатор библиотеки по умолчанию.
Если вы используете библиотеку в асинхронной веб -структуре, такой как FASTAPI, то вы уже находитесь в асинхронной среде, так что вы можете отображать компоненты так же просто, как это: await HTMY().render(my_root_component) .
Если вы пытаетесь запустить рендеринг в среде синхронизации, например, локальный сценарий или CLI, то вам сначала необходимо обернуть рендеринг в асинхронную задачу и выполнить эту задачу с помощью asyncio.run() :
import asyncio
from htmy import HTMY , html
async def render_page () -> None :
page = (
html . DOCTYPE . html ,
html . html (
html . body (
html . h1 ( "Hello World!" ),
html . p ( "This page was rendered by " , html . code ( "htmy" )),
),
)
)
result = await HTMY (). render ( page )
print ( result )
if __name__ == "__main__" :
asyncio . run ( render_page ()) Как вы могли видеть из приведенных выше примеров кода, у каждого компонента есть context: Context аргумент, который мы до сих пор не использовали. Контекст - это способ поделиться данными со всем поддереем компонента без «бурения опоры».
Контекст (технически Mapping ) полностью управляется рендерером. Компоненты поставщика контекста (любой класс с синхронизацией или Async htmy_context() -> Context ) добавляют новые данные в контекст, чтобы сделать его доступными для компонентов в их подборе, и компоненты могут просто взять то, что им нужно из контекста.
Нет никаких ограничений на то, что может быть в контексте, его можно использовать для всего, что нужно применению, например, что делает текущего пользователя, предпочтения пользовательского интерфейса, темы или форматер, доступных для компонентов. Фактически, встроенные компоненты получают свое Formatter из контекста, если он содержит один, чтобы позволить настройке имени свойства и форматирования значений.
Вот пример поставщика контекста и внедрения потребителей:
import asyncio
from htmy import HTMY , Component , ComponentType , Context , component , html
class UserContext :
def __init__ ( self , * children : ComponentType , username : str , theme : str ) -> None :
self . _children = children
self . username = username
self . theme = theme
def htmy_context ( self ) -> Context :
# Context provider implementation.
return { UserContext : self }
def htmy ( self , context : Context ) -> Component :
# Context providers must also be components, as they just
# wrap some children components in their context.
return self . _children
@ classmethod
def from_context ( cls , context : Context ) -> "UserContext" :
user_context = context [ cls ]
if isinstance ( user_context , UserContext ):
return user_context
raise TypeError ( "Invalid user context." )
@ component
def welcome_page ( text : str , context : Context ) -> Component :
# Get user information from the context.
user = UserContext . from_context ( context )
return (
html . DOCTYPE . html ,
html . html (
html . body (
html . h1 ( text , html . strong ( user . username )),
data_theme = user . theme ,
),
),
)
async def render_welcome_page () -> None :
page = UserContext (
welcome_page ( "Welcome back " ),
username = "John" ,
theme = "dark" ,
)
result = await HTMY (). render ( page )
print ( result )
if __name__ == "__main__" :
asyncio . run ( render_welcome_page ()) Конечно, вы можете полагаться на встроенные утилиты, связанные с контекстом, такие как классы ContextAware или WithContext для удобного и напечатанного контекста с меньшим количеством кода шаблона.
Как упоминалось ранее, встроенный класс Formatter отвечает за имя атрибута тега и форматирование значений. Вы можете полностью переопределить или расширить встроенное поведение форматирования, просто расширив этот класс или добавив новые правила в его экземпляр, а затем добавив пользовательский экземпляр в контекст, либо непосредственно в HTMY , либо HTMY.render() , либо в компонент поставщика контекста.
Это правила форматирования тега по умолчанию:
_ -> - ), если имя атрибута не запускается или не заканчивается подчеркиванием, и в этом случае, ведущие и следственные недостатки, не удалены, а остальное имя атрибута сохраняется. Например, data_theme="dark" преобразуется в data-theme="dark" , но _data_theme="dark" будет в конечном итоге как data_theme="dark" в визуализированном тексте. Что еще более важно, class_="text-danger" , _class="text-danger" , _class__="text-danger" все преобразуются в class="text-danger" , и _for="my-input" или for_="my_input" станут for="my-input" .bool преобразуются в строки ( "true" и "false" ).XBool.true атрибутов преобразуются в пустую строку, а значения XBool.false пропущены (только имя атрибута отображается).date и datetime преобразуются в строки ISO. Компонент ErrorBoundary полезен, если вы хотите, чтобы ваше приложение исчезло изящно (например, отображение сообщения об ошибке) вместо того, чтобы увеличить ошибку HTTP.
Граница ошибки завершает поддеревание компонента компонента. Когда рендерер встречает ошибку, компонент ErrorBoundary , он попытается отобразить обернутый контент. Если рендеринг не удается с исключением в любой точке в подтерею ErrorBoundary , рендерер автоматически вернется к компоненту, который вы присваивали своему свойству fallback ErrorBoundary .
При желании вы можете определить, какие ошибки могут обрабатывать границу ошибок, давая вам тонкий контроль над обработкой ошибок.
В общем, компонент должен быть асинхронным, если он должен ждать некоторого асинхрового вызова внутри.
Если компонент выполняет потенциально «длительный» синхронное вызов, настоятельно рекомендуется делегировать этот вызов работнику, ожидающему его (таким образом, делая компонент асинхронизированием). Это может быть сделано, например, с помощью утилиты anyio 's to_thread , starlette (или fastapi ) run_in_threadpool() и так далее. Цель здесь состоит в том, чтобы не блокировать петлю событий Asyncio, так как это может привести к проблемам с производительностью.
Во всех других случаях лучше всего использовать компоненты синхронизации.
FOSTAPI:
На одном конце спектра существуют полные приложения, которые объединяют приложения сервера (Python) и Client (JavaScript) с всем управлением состоянием и синхронизацией в один пакет Python (в некоторых случаях дополнительный JavaScript). Некоторые из самых популярных примеров: Reflex, Nicegui, Reactpy и Fastui.
Основным преимуществом этих рамок является быстрое прототипирование приложений и очень удобный опыт разработчика (по крайней мере, до тех пор, пока вы остаетесь в рамках встроенного набора функций структуры). В обмен на это они очень самоуверенные (от компонентов до управления инструментом и управления государством), базовая инженерия очень сложна, развертывание и масштабирование может быть жестким или дорогостоящим, и их может быть трудно перейти. Даже с этими предостережениями они могут быть очень хорошим выбором для внутренних инструментов и прототипов приложений.
На другом конце спектра - простых двигателей рендеринга - преобладает механизм шаблона джинджи, который является безопасным выбором, как и было и будет уже долгое время. Основными недостатками с джинджа является отсутствие хорошей поддержки IDE, полное отсутствие поддержки статического анализа кода и (субъективно) уродливый синтаксис.
Затем есть инструменты, направленные на среднюю землю, обычно путем предоставления большинства преимуществ и недостатков полных фреймворков приложений, оставляя при этом управление состоянием, общение с клиентами и динамические обновления пользовательского интерфейса для решения пользователя, часто с некоторым уровнем поддержки HTMX. Эта группа включает в себя библиотеки, такие как FASTHTML и Ludic.
Основной целью htmy является асинхровый двигатель рендеринга с чистым питоном, который является настолько простым , обслуживаемым и настраиваемым , насколько это возможно, в то же время предоставляя все строительные блоки для (удобно) создания сложных и обслуживаемых приложений.
Библиотека направлена на то, чтобы минимизировать его зависимости. В настоящее время требуются следующие зависимости:
anyio : для асинхронных файловых операций и сети.async-lru : для асинхронного кэширования.markdown : для каркасного анализа. Используйте ruff для личинга и форматирования, mypy для анализа статического кода и pytest для тестирования.
Документация построена с mkdocs-material и mkdocstrings .
Все взносы приветствуются, включая больше документации, примеров, кода и тестов. Даже вопросы.
Пакет открыт в условиях лицензии MIT.