Coverage for src/lexigram/web/decorators.py: 22%

37 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 04:37 +0800

1"""Decorator composition utilities. 

2 

3Allows composing multiple decorators into a single unified decorator. 

4""" 

5 

6from __future__ import annotations 

7 

8from collections.abc import Callable 

9from typing import Any, TypeVar, cast 

10 

11F = TypeVar("F", bound=Callable) 

12 

13 

14def compose(*decorators: Callable[[F], F]) -> Callable[[F], F]: 

15 """Compose multiple decorators into one. 

16 

17 The decorators are applied in order from last to first (like function composition). 

18 

19 Usage: 

20 @compose( 

21 version("2"), 

22 use_guards(AuthGuard), 

23 use_interceptors(AuditLogInterceptor), 

24 ) 

25 @post("/admin/action") 

26 async def perform_action(self, data: ActionDTO): 

27 ... 

28 """ 

29 

30 def decorator(func: F) -> F: 

31 result = func 

32 # Apply decorators in reverse order so they're applied in the correct order 

33 for dec in reversed(decorators): 

34 result = dec(result) 

35 return result 

36 

37 return decorator 

38 

39 

40def api_controller( 

41 prefix: str = "", 

42 guards: list[Any] | None = None, 

43 interceptors: list[Any] | None = None, 

44 middleware: list[Any] | None = None, 

45 version: str | None = None, 

46) -> Callable[[type], type]: 

47 """Create a controller class with shared configuration. 

48 

49 Usage: 

50 @api_controller( 

51 prefix="/api/v1", 

52 guards=[AuthGuard], 

53 interceptors=[LoggingInterceptor], 

54 version="1", 

55 ) 

56 class UserController(Controller): 

57 ... 

58 """ 

59 

60 def decorator(cls: type) -> type: 

61 # Store metadata on the class using setattr to prevent mypy attr-defined errors 

62 if not hasattr(cls, "_controller_config"): 

63 cast("Any", cls)._controller_config = {} 

64 

65 config = cast("Any", cls)._controller_config 

66 config["prefix"] = prefix 

67 config["guards"] = guards or [] 

68 config["interceptors"] = interceptors or [] 

69 config["middleware"] = middleware or [] 

70 config["version"] = version 

71 

72 return cls 

73 

74 return decorator 

75 

76 

77def merge_metadata( 

78 *metadatas: dict[str, Any], 

79 priority: str = "last", 

80) -> dict[str, Any]: 

81 """Merge multiple metadata dictionaries. 

82 

83 Args: 

84 *metadatas: Multiple metadata dicts to merge 

85 priority: "first" or "last" - which dict takes precedence on conflicts 

86 

87 Usage: 

88 meta1 = {"guards": [AuthGuard], "version": "1"} 

89 meta2 = {"guards": [AdminGuard], "version": "2"} 

90 merged = merge_metadata(meta1, meta2, priority="last") 

91 # Result: {"guards": [AdminGuard], "version": "2"} 

92 """ 

93 result: dict[str, Any] = {} 

94 

95 for meta in metadatas: 

96 for key, value in meta.items(): 

97 if key not in result: 

98 result[key] = value 

99 elif priority == "last": 

100 # Merge lists, override others 

101 if isinstance(value, list) and isinstance(result[key], list): 

102 result[key] = result[key] + value 

103 else: 

104 result[key] = value 

105 elif isinstance(result[key], list) and isinstance(value, list): 

106 result[key] = value + result[key] 

107 # Don't override existing non-list values 

108 

109 return result 

110 

111 

112__all__ = [ 

113 "api_controller", 

114 "compose", 

115 "merge_metadata", 

116]