ซอร์สโค้ด : https://github.com/volfpeter/htmy
เอกสารและตัวอย่าง : https://volfpeter.github.io/htmy
htmyasync , เครื่องยนต์เรนเดอร์ Pure-Python
ErrorBoundary สำหรับการจัดการข้อผิดพลาดที่สง่างามแพ็คเกจมีอยู่ใน PYPI และสามารถติดตั้งได้ด้วย:
$ pip install htmy ห้องสมุดทั้งหมด-จากเอ็นจิ้นการเรนเดอร์ไปจนถึงส่วนประกอบในตัว-ถูกสร้างขึ้นรอบ ๆ โปรโตคอลง่าย ๆ สองสามตัวและคลาสยูทิลิตี้ที่เรียบง่ายจำนวนหนึ่ง ซึ่งหมายความว่าคุณสามารถปรับแต่งขยายหรือแทนที่ทุกอย่างในห้องสมุดได้อย่างง่ายดาย ใช่แม้แต่เอ็นจิ้นเรนเดอร์ ชิ้นส่วนที่เหลือจะทำงานต่อไปตามที่คาดไว้
นอกจากนี้ห้องสมุดไม่ได้พึ่งพาคุณสมบัติ Python ขั้นสูงเช่น metaclasses หรือ descriptors นอกจากนี้ยังไม่มีคลาสฐานที่ซับซ้อนและสิ่งที่คล้ายกัน แม้แต่วิศวกรรุ่นเยาว์ก็สามารถเข้าใจพัฒนาและดีบักแอปพลิเคชันที่สร้างขึ้นด้วย htmy
ทุกคลาสที่มีการซิงค์หรือ Async htmy(context: Context) -> Component เป็นองค์ประกอบ htmy (ในทางเทคนิค HTMYComponentType ) สตริงยังเป็นส่วนประกอบเช่นเดียวกับรายการหรือ tuples ของวัตถุ HTMYComponentType หรือสตริง
การใช้ชื่อวิธีนี้ช่วยให้สามารถแปลงวัตถุธุรกิจใด ๆ ของคุณ (จากรุ่น TypedDicts S หรือ pydantic เป็นคลาส ORM) เป็นส่วนประกอบโดยไม่ต้องกลัวการชนกันของชื่อด้วยเครื่องมืออื่น ๆ
การสนับสนุน Async ทำให้สามารถโหลดข้อมูลหรือดำเนินการตามตรรกะทางธุรกิจของ Async ในส่วนประกอบของคุณ สิ่งนี้สามารถลดปริมาณหม้อไอน้ำที่คุณต้องเขียนในบางกรณีและยังให้อิสระในการแยกการแสดงผลและตรรกะที่ไม่แสดงผลในทางที่คุณเห็นว่าเหมาะสม
ตัวอย่าง:
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 Decorator ที่สามารถใช้กับการซิงค์หรือ 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 และส่วนประกอบ MD สำหรับการโหลดการแยกวิเคราะห์การแปลงและการแสดงเนื้อหา Markdowni18n : สาธารณูปโภคสำหรับ ASYNC, ความเป็นสากลจาก JSONBaseTag , TagWithProps , Tag , WildcardTag : คลาสพื้นฐานสำหรับแท็ก XML ที่กำหนดเองErrorBoundary , Fragment , SafeStr , WithContext : ยูทิลิตี้สำหรับการจัดการข้อผิดพลาด, wrappers ส่วนประกอบ, ผู้ให้บริการบริบทและการจัดรูปแบบSnippet : คลาสยูทิลิตี้สำหรับการโหลดและปรับแต่งตัวอย่างเอกสารจากระบบไฟล์etree.ETreeConverter : ยูทิลิตี้ที่แปลง XML เป็นแผนผังส่วนประกอบด้วยการรองรับส่วนประกอบ HTMY ที่กำหนดเอง htmy.HTMY เป็นตัวแสดงผลเริ่มต้นในตัวของไลบรารี
หากคุณใช้ไลบรารีในกรอบเว็บแบบ async เช่น fastapi คุณจะอยู่ในสภาพแวดล้อม async แล้วดังนั้นคุณสามารถแสดงส่วนประกอบได้เช่นเดียวกับนี้: await HTMY().render(my_root_component)
หากคุณกำลังพยายามเรียกใช้ Renderer ในสภาพแวดล้อมการซิงค์เช่นสคริปต์ท้องถิ่นหรือ CLI คุณต้องปิดตัวเรนเดอร์ในงาน async และดำเนินการงานนั้นด้วย 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 ) เพิ่มข้อมูลใหม่ลงในบริบทเพื่อให้สามารถใช้งานส่วนประกอบในทรีย่อยของพวกเขาและส่วนประกอบสามารถนำสิ่งที่พวกเขาต้องการจากบริบท
ไม่มีข้อ จำกัด เกี่ยวกับสิ่งที่สามารถทำได้ในบริบทมันสามารถใช้กับสิ่งที่แอปพลิเคชันต้องการเช่นการสร้างผู้ใช้ปัจจุบันการตั้งค่า 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 :
# 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 หรือ classes 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
ขอบเขตข้อผิดพลาดจะห่อทรีย่อยส่วนประกอบส่วนประกอบ เมื่อ Renderer พบส่วนประกอบ ErrorBoundary มันจะพยายามแสดงเนื้อหาที่ห่อหุ้ม หากการเรนเดอร์ล้มเหลวโดยมีข้อยกเว้น ณ จุดใด ๆ ในทรีย่อยของ ErrorBoundary การแสดงผลจะกลับไปที่ส่วนประกอบที่คุณกำหนดให้กับคุณสมบัติ fallback ของ ErrorBoundary โดยอัตโนมัติ
ทางเลือกคุณสามารถกำหนดข้อผิดพลาดที่ขอบเขตข้อผิดพลาดสามารถจัดการได้ทำให้คุณควบคุมการจัดการข้อผิดพลาดได้อย่างดี
โดยทั่วไปองค์ประกอบควรเป็น async หากต้องรอการโทรบางอย่างภายใน
หากส่วนประกอบดำเนินการโทรแบบซิงโครนัส "วิ่งระยะยาว" ที่อาจเกิดขึ้นได้ขอแนะนำอย่างยิ่งให้มอบหมายให้โทรไปที่ด้ายคนงานกำลังรอคอย (ทำให้ส่วนประกอบ async) สิ่งนี้สามารถทำได้เช่นด้วยยูทิลิตี้ to_thread ของ anyio , starlette 's (หรือ fastapi ' s) run_in_threadpool() และอื่น ๆ เป้าหมายที่นี่คือการหลีกเลี่ยงการปิดกั้นเหตุการณ์ Asyncio ซึ่งอาจนำไปสู่ปัญหาด้านประสิทธิภาพ
ในกรณีอื่น ๆ ทั้งหมดควรใช้ส่วนประกอบซิงค์
fastapi:
ที่ปลายด้านหนึ่งของสเปกตรัมมีเฟรมเวิร์กแอปพลิเคชันที่สมบูรณ์ที่รวมแอปพลิเคชันเซิร์ฟเวอร์ (Python) และไคลเอนต์ (JavaScript) เข้ากับการจัดการสถานะทั้งหมดและการซิงโครไนซ์ลงในแพ็คเกจ Python เดียว (ในบางกรณี JavaScript เพิ่มเติม) ตัวอย่างที่ได้รับความนิยมมากที่สุดคือ: Reflex, NiceGui, ReactPy และ Fastui
ประโยชน์หลักของเฟรมเวิร์กเหล่านี้คือการสร้างต้นแบบแอปพลิเคชันอย่างรวดเร็วและประสบการณ์นักพัฒนาที่สะดวกมาก (อย่างน้อยก็ตราบใดที่คุณอยู่ในชุดคุณสมบัติในตัวของเฟรมเวิร์ก) เพื่อแลกกับสิ่งนั้นพวกเขามีความเห็นอย่างมาก (จากส่วนประกอบไปจนถึงการจัดการส่วนหน้าและการจัดการของรัฐ) วิศวกรรมพื้นฐานมีความซับซ้อนมากการปรับใช้และการปรับขนาดอาจเป็นเรื่องยากหรือมีค่าใช้จ่ายสูงและพวกเขาสามารถย้ายออกไปได้ยาก แม้จะมีคำเตือนเหล่านี้พวกเขาก็เป็นตัวเลือกที่ดีมากสำหรับเครื่องมือภายในและการสร้างต้นแบบแอปพลิเคชัน
ปลายอีกด้านของสเปกตรัม - เครื่องยนต์การเรนเดอร์ธรรมดา - ถูกครอบงำโดยเครื่องยนต์ Jinja templating ซึ่งเป็นตัวเลือกที่ปลอดภัยอย่างที่เคยเป็นมาและจะอยู่รอบ ๆ เป็นเวลานาน ข้อเสียเปรียบหลักของ Jinja คือการขาดการสนับสนุน IDE ที่ดีการขาดการสนับสนุนการวิเคราะห์รหัสแบบคงที่อย่างสมบูรณ์และไวยากรณ์ที่น่าเกลียด
จากนั้นมีเครื่องมือที่มีจุดมุ่งหมายสำหรับดินแดนกลางมักจะให้ประโยชน์และข้อเสียส่วนใหญ่ของกรอบแอปพลิเคชันที่สมบูรณ์ในขณะที่ออกจากการจัดการสถานะการสื่อสารลูกค้าเซิร์ฟเวอร์และการอัปเดต UI แบบไดนามิกสำหรับผู้ใช้ในการแก้ปัญหาบ่อยครั้งด้วยการสนับสนุน HTMX ในระดับหนึ่ง กลุ่มนี้มีห้องสมุดเช่น Fasthtml และ Ludic
เป้าหมายหลักของ htmy คือการเป็นเอ็นจิ้นการเรนเดอร์ แบบเพ่ง Pure-Python ซึ่งเป็น เรื่องง่าย บำรุงรักษา และ ปรับแต่งได้ มากที่สุดเท่าที่จะทำได้
ห้องสมุดมีจุดมุ่งหมายเพื่อให้การพึ่งพาน้อยที่สุด ปัจจุบันจำเป็นต้องมีการพึ่งพาต่อไปนี้:
anyio : สำหรับการดำเนินการไฟล์และเครือข่าย asyncasync-lru : สำหรับการแคช Asyncmarkdown : สำหรับการแยกวิเคราะห์ Markdown ใช้ ruff สำหรับการผ้าสำลีและการจัดรูปแบบ mypy สำหรับการวิเคราะห์รหัสแบบคงที่และ pytest สำหรับการทดสอบ
เอกสารนี้สร้างขึ้นด้วย mkdocs-material และ mkdocstrings
ยินดีต้อนรับการมีส่วนร่วมทั้งหมดรวมถึงเอกสารเพิ่มเติมตัวอย่างรหัสและการทดสอบ แม้แต่คำถาม
แพ็คเกจเปิดโล่งภายใต้เงื่อนไขของใบอนุญาต MIT