Coverage for src / lexigram / ui / decorators.py: 100%

16 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-10 04:11 +0800

1"""Decorators for UI components — metadata tagging for registry and render pipeline.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Callable 

6import functools 

7from typing import Any, TypeVar 

8 

9F = TypeVar("F", bound=Callable[..., Any]) 

10 

11__all__ = [ 

12 "component", 

13] 

14 

15 

16def component( 

17 name: str | None = None, 

18 *, 

19 cacheable: bool = False, 

20) -> Callable[[F], F]: 

21 """Mark a callable as a named UI component and attach component metadata. 

22 

23 Tags the decorated function with ``__component_name__`` and 

24 ``__component_cacheable__`` attributes so that component registries, 

25 debug tooling, and render pipelines can discover and identify components 

26 by name without relying on ``__qualname__``. 

27 

28 This decorator does **not** register the component in any global registry 

29 by itself — registration is handled by the DI container via the 

30 :class:`~lexigram.ui.di.provider.UIProvider`. The decorator provides 

31 the metadata that the provider reads during component discovery. 

32 

33 Args: 

34 name: Logical component name used for registration and cache keys. 

35 Defaults to the decorated function's ``__name__``. 

36 cacheable: When ``True``, signals that the component's rendered output 

37 may be cached by the render pipeline. Defaults to ``False``. 

38 

39 Returns: 

40 Decorator that attaches component metadata to the target callable. 

41 

42 Example:: 

43 

44 @component("user_card", cacheable=True) 

45 def user_card(user: User) -> str: 

46 return render_to_string( 

47 el("div", {"class": "card"}, user.display_name) 

48 ) 

49 

50 @component() 

51 def avatar(src: str, alt: str = "") -> str: 

52 return render_to_string(el("img", {"src": src, "alt": alt})) 

53 """ 

54 

55 def decorator(fn: F) -> F: 

56 component_name = name or fn.__name__ 

57 

58 @functools.wraps(fn) 

59 def wrapper(*args: Any, **kwargs: Any) -> Any: 

60 return fn(*args, **kwargs) 

61 

62 wrapper.__component_name__ = component_name # type: ignore[attr-defined] 

63 wrapper.__component_cacheable__ = cacheable # type: ignore[attr-defined] 

64 return wrapper # type: ignore[return-value] 

65 

66 return decorator