htmy
--异步,纯 Python渲染引擎。
核心特性
异步优先,让您充分利用现代异步工具[1]。 强大的,类似 React 的上下文支持,避免属性钻取。 同步和异步函数组件,带有装饰器语法。 内置所有基础HTML标签。 Markdown支持,附带自定义工具。 异步,基于 JSON 的国际化。 内置易用的 ErrorBoundary
组件,用于优雅的错误处理。无偏见:自由选择后端、CSS 和 JS 框架,按照您希望的方式使用它们。 从渲染引擎到组件,格式化和上下文管理,一切都易于定制。 自动和可定制的属性名转换,从蛇形命名到短横线命名。 全类型注解。
安装
该包在 PyPI 上可用,可以通过以下命令安装:
$ pip install htmy
概念
整个库——从渲染引擎本身到内置组件——都是围绕几个简单的协议和一些简单的实用类构建的。这意味着您可以轻松定制、扩展或替换库中的几乎所有内容。是的,甚至是渲染引擎。剩余的部分将如预期那样继续工作。
此外,该库不依赖于高级 Python 特性,如元类或描述符。也没有复杂的基类等。即使是初级工程师也能理解和调试使用htmy
构建的应用程序。
组件
每个具有同步或异步htmy(context: Context) -> Component
方法的类都是htmy
组件(技术上是HTMYComponentType
)。字符串也是组件,以及HTMYComponentType
或字符串对象的列表或元组。
使用这种方法名可以让您将任何业务对象(从TypedDicts
或pydantic
模型到 ORM 类)转换为组件,而不必担心与其他工具的名称冲突。
异步支持使得在组件中直接加载数据或执行异步业务逻辑成为可能。这可以在某些情况下减少您需要编写的样板代码数量,并且还为您提供了自由,以任何您认为合适的方式分割渲染和非渲染逻辑。
示例:
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:
# 注意这里返回的是列表。`HTMYComponentType | str`对象的列表或元组也是组件。
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="foo@example.com"),
User(username="Bar", name="Bar", email="bar@example.com"),
]
)
)
htmy
还提供了一个@component
装饰器,可以用于同步或异步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: # 函数组件的第一个参数是它们的“属性”,即它们需要的数据。 # 第二个参数是渲染上下文。 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: # 这个组件中没有需要等待的内容,因此是同步的。 # 注意我们只将“属性”传递给 user_row()组件(好吧,函数组件包装器)。 # 上下文将在渲染期间传递给包装器。 return [user_row(user) for user in users]
user_table = html.table( user_rows( [ User(username="Foo", name="Foo", email="foo@example.com"), User(username="Bar", name="Bar", email="bar@example.com"), ] ) )
内置组件
htmy
提供了丰富的内置工具和组件,用于 HTML 和其他用例:
html
模块:一套完整的基础 HTML 标签[2]。md
:MarkdownParser
实用工具和MD
组件,用于加载、解析、转换和渲染 Markdown 内容。i18n
:异步,基于 JSON 的国际化工具。BaseTag
,TagWithProps
,Tag
,WildcardTag
:自定义 XML 标签的基类。ErrorBoundary
,Fragment
,SafeStr
,WithContext
:用于错误处理、组件包装器、上下文提供者和格式化的工具。Snippet
:从文件系统加载和自定义文档片段的实用类。etree.ETreeConverter
:将 XML 转换为组件树的实用工具,支持自定义 HTMY 组件。
渲染
htmy.HTMY
是库中内置的默认渲染器。
如果您在像FastAPI[3]这样的异步 Web 框架中使用库,那么您已经在异步环境中,因此您可以像这样简单地渲染组件: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
)完全由渲染器管理。上下文提供者组件(任何具有同步或异步htmy_context() -> Context
方法的类)向上下文中添加新数据,使其对它们子树中的组件可用,组件可以简单地从上下文中获取它们需要的内容。
上下文中可以包含的内容没有任何限制,它可以用于应用程序需要的任何内容,例如使当前用户、UI 偏好、主题或格式化器对组件可用。实际上,内置组件如果上下文中包含一个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:
# 上下文提供者实现。
return {UserContext: self}
def htmy(self, context: Context) -> Component:
# 上下文提供者也必须是组件,因为它们只是
# 在它们的上下文中包装一些子组件。
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:
# 从上下文中获取用户信息。
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
的子树中的任何点渲染失败并抛出异常,渲染器将自动回退到您分配给ErrorBoundary
的fallback
属性的组件。
您还可以选择性地定义错误边界可以处理哪些错误,为您提供精细的错误处理控制。
同步还是异步?
通常,如果组件必须在内部等待某些异步调用,则应该是异步的。
如果一个组件执行可能“长时间运行”的同步调用,强烈建议将该调用委托给工作线程并等待它(从而使组件变为异步)。这可以使用anyio
的to_thread
实用工具[4],starlette
的(或fastapi
的)run_in_threadpool()
等来完成。这里的目标是避免阻塞 asyncio 事件循环,因为这可能导致性能问题。
在所有其他情况下,最好使用同步组件。
框架集成
FastAPI:
FastHX[5]
为什么
在谱系的一端,有将服务器(Python)和客户端(JavaScript)应用程序以及整个状态管理和同步整合到一个 Python(在某些情况下还有额外的 JavaScript)包中的完整应用程序框架。一些最受欢迎的例子包括:Reflex[6],NiceGUI[7],ReactPy[8],和FastUI[9]。
这些框架的主要好处是快速应用原型制作和非常方便的开发者体验(至少在您停留在框架的内置功能集内时)。作为交换,它们非常主观(从组件到前端工具和状态管理),底层工程非常复杂,部署和扩展可能很难或成本很高,它们很难迁移。即使有这些限制,它们可以是内部工具和应用原型制作的非常好的选择。
另一端的谱系——纯渲染引擎——由Jinja[10]模板引擎主导,这是一个安全的选择,因为它已经存在并将长期存在。Jinja 的主要缺点是缺乏良好的 IDE 支持,完全缺乏静态代码分析支持,以及(主观上的)丑陋的语法。
然后是那些旨在中间地带的工具,通常通过提供大部分完整应用程序框架的好处和缺点,同时将状态管理、客户端-服务器通信和动态 UI 更新留给用户解决,通常带有某种程度的HTMX[11]支持。这个群体包括像FastHTML[12]和Ludic[13]这样的库。
htmy
的主要目标是成为一个异步的,纯 Python 渲染引擎,尽可能简单、可维护和可定制,同时仍然提供所有构建(方便地)复杂和可维护应用程序的构建块。
依赖
该库旨在最小化其依赖。目前需要以下依赖:
anyio
:用于异步文件操作和网络。async-lru
:用于异步缓存。markdown
:用于 Markdown 解析。
开发
使用ruff
进行 linting 和格式化,使用mypy
进行静态代码分析,使用pytest
进行测试。
文档是用mkdocs-material
和mkdocstrings
构建的。
贡献
欢迎所有贡献,包括更多文档、示例、代码和测试。甚至问题。
许可证 - MIT
该包在MIT 许可证[14]条件下开源。
现代异步工具:https://github.com/timofurrer/awesome-asyncio
[2]基础 HTML 标签:https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility
[3]FastAPI:https://fastapi.tiangolo.com/
[4]实用工具:https://anyio.readthedocs.io/en/stable/threads.html
[5]FastHX:https://github.com/volfpeter/fasthx
[6]Reflex:https://github.com/reflex-dev/reflex
[7]NiceGUI:https://github.com/zauberzeug/nicegui/
[8]ReactPy:https://github.com/reactive-python/reactpy
[9]FastUI:https://github.com/pydantic/FastUI
[10]Jinja:https://jinja.palletsprojects.com
[11]HTMX:https://htmx.org/
[12]FastHTML:https://github.com/answerdotai/fasthtml
[13]Ludic:https://github.com/getludic/ludic
[14]MIT 许可证:https://choosealicense.com/licenses/mit/