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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""Decorator composition utilities.
3Allows composing multiple decorators into a single unified decorator.
4"""
6from __future__ import annotations
8from collections.abc import Callable
9from typing import Any, TypeVar, cast
11F = TypeVar("F", bound=Callable)
14def compose(*decorators: Callable[[F], F]) -> Callable[[F], F]:
15 """Compose multiple decorators into one.
17 The decorators are applied in order from last to first (like function composition).
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 """
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
37 return decorator
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.
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 """
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 = {}
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
72 return cls
74 return decorator
77def merge_metadata(
78 *metadatas: dict[str, Any],
79 priority: str = "last",
80) -> dict[str, Any]:
81 """Merge multiple metadata dictionaries.
83 Args:
84 *metadatas: Multiple metadata dicts to merge
85 priority: "first" or "last" - which dict takes precedence on conflicts
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] = {}
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
109 return result
112__all__ = [
113 "api_controller",
114 "compose",
115 "merge_metadata",
116]