Coverage for src/lexigram/graphql/schema/decorators.py: 22%

80 statements  

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

1"""GraphQL schema decorators. 

2 

3This module provides decorator functions for defining GraphQL 

4resolvers, queries, mutations, and subscriptions using Strawberry. 

5""" 

6 

7from __future__ import annotations 

8 

9from collections.abc import Awaitable, Callable 

10import functools 

11from typing import ( 

12 TYPE_CHECKING, 

13 Any, 

14 ParamSpec, 

15 TypeVar, 

16 cast, 

17) 

18 

19import strawberry 

20from strawberry import Info 

21 

22from lexigram.logging import get_logger 

23 

24if TYPE_CHECKING: 

25 from lexigram.graphql.core.context import GraphQLContext 

26 

27logger = get_logger(__name__) 

28 

29P = ParamSpec("P") 

30T = TypeVar("T") 

31R = TypeVar("R") 

32 

33 

34def query( 

35 name: str | None = None, 

36 description: str | None = None, 

37 deprecation_reason: str | None = None, 

38 permission_classes: list[Any] | None = None, 

39) -> Callable[[Callable[P, T]], Callable[P, Any]]: 

40 """Decorator for marking a method as a GraphQL query. 

41 

42 This is a wrapper around Strawberry's field decorator with 

43 additional Lexigram-specific functionality. 

44 

45 Args: 

46 name: Optional field name (defaults to function name). 

47 description: Field description for documentation. 

48 deprecation_reason: If set, marks the field as deprecated. 

49 permission_classes: List of permission classes for authorization. 

50 

51 Returns: 

52 Decorated function. 

53 

54 Example: 

55 ```python 

56 @strawberry.type 

57 class Query: 

58 @query(description="Get user by ID") 

59 async def user(self, info: Info, id: str) -> User: 

60 return await get_user(id) 

61 ``` 

62 """ 

63 

64 def decorator(func: Callable[P, T]) -> Callable[P, T]: 

65 # Build kwargs for strawberry.field 

66 field_kwargs: dict[str, Any] = {} 

67 

68 if name: 

69 field_kwargs["name"] = name 

70 if description: 

71 field_kwargs["description"] = description 

72 if deprecation_reason: 

73 field_kwargs["deprecation_reason"] = deprecation_reason 

74 if permission_classes: 

75 field_kwargs["permission_classes"] = permission_classes 

76 

77 # Apply strawberry field decorator 

78 return cast("Callable[P, T]", strawberry.field(**field_kwargs)(func)) 

79 

80 return decorator 

81 

82 

83def mutation( 

84 name: str | None = None, 

85 description: str | None = None, 

86 deprecation_reason: str | None = None, 

87 permission_classes: list[Any] | None = None, 

88) -> Callable[[Callable[P, T]], Callable[P, Any]]: 

89 """Decorator for marking a method as a GraphQL mutation. 

90 

91 This is a wrapper around Strawberry's mutation decorator with 

92 additional Lexigram-specific functionality. 

93 

94 Args: 

95 name: Optional field name (defaults to function name). 

96 description: Field description for documentation. 

97 deprecation_reason: If set, marks the field as deprecated. 

98 permission_classes: List of permission classes for authorization. 

99 

100 Returns: 

101 Decorated function. 

102 

103 Example: 

104 ```python 

105 @strawberry.type 

106 class Mutation: 

107 @mutation(description="Create a new user") 

108 async def create_user( 

109 self, info: Info, input: CreateUserInput 

110 ) -> User: 

111 return await create_user(input) 

112 ``` 

113 """ 

114 

115 def decorator(func: Callable[P, T]) -> Callable[P, T]: 

116 # Build kwargs for strawberry.mutation 

117 mutation_kwargs: dict[str, Any] = {} 

118 

119 if name: 

120 mutation_kwargs["name"] = name 

121 if description: 

122 mutation_kwargs["description"] = description 

123 if deprecation_reason: 

124 mutation_kwargs["deprecation_reason"] = deprecation_reason 

125 if permission_classes: 

126 mutation_kwargs["permission_classes"] = permission_classes 

127 

128 # Apply strawberry mutation decorator 

129 return cast("Callable[P, T]", strawberry.mutation(**mutation_kwargs)(func)) 

130 

131 return decorator 

132 

133 

134def subscription( 

135 name: str | None = None, 

136 description: str | None = None, 

137 deprecation_reason: str | None = None, 

138 permission_classes: list[Any] | None = None, 

139) -> Callable[[Callable[P, T]], Callable[P, Any]]: 

140 """Decorator for marking a method as a GraphQL subscription. 

141 

142 This is a wrapper around Strawberry's subscription decorator with 

143 additional Lexigram-specific functionality. 

144 

145 Args: 

146 name: Optional field name (defaults to function name). 

147 description: Field description for documentation. 

148 deprecation_reason: If set, marks the field as deprecated. 

149 permission_classes: List of permission classes for authorization. 

150 

151 Returns: 

152 Decorated function. 

153 

154 Example: 

155 ```python 

156 @strawberry.type 

157 class Subscription: 

158 @subscription(description="Subscribe to messages") 

159 async def messages( 

160 self, info: Info, room_id: str 

161 ) -> AsyncGenerator[Message, None]: 

162 async for msg in message_stream(room_id): 

163 yield msg 

164 ``` 

165 """ 

166 

167 def decorator(func: Callable[P, T]) -> Callable[P, T]: 

168 # Build kwargs for strawberry.subscription 

169 sub_kwargs: dict[str, Any] = {} 

170 

171 if name: 

172 sub_kwargs["name"] = name 

173 if description: 

174 sub_kwargs["description"] = description 

175 if deprecation_reason: 

176 sub_kwargs["deprecation_reason"] = deprecation_reason 

177 if permission_classes: 

178 sub_kwargs["permission_classes"] = permission_classes 

179 

180 # Apply strawberry subscription decorator 

181 return cast("Callable[P, T]", strawberry.subscription(**sub_kwargs)(func)) 

182 

183 return decorator 

184 

185 

186def field( 

187 name: str | None = None, 

188 description: str | None = None, 

189 deprecation_reason: str | None = None, 

190 default: Any = strawberry.UNSET, 

191 default_factory: Callable[[], Any] | None = None, 

192 permission_classes: list[Any] | None = None, 

193) -> Any: 

194 """Define a GraphQL field. 

195 

196 This is a wrapper around Strawberry's field function with 

197 additional Lexigram-specific functionality. 

198 

199 Args: 

200 name: Optional field name. 

201 description: Field description. 

202 deprecation_reason: Deprecation reason if deprecated. 

203 default: Default value. 

204 default_factory: Factory for default value. 

205 permission_classes: Permission classes for authorization. 

206 

207 Returns: 

208 Strawberry field. 

209 

210 Example: 

211 ```python 

212 @strawberry.type 

213 class User: 

214 id: str 

215 name: str = field(description="User's full name") 

216 email: str = field(description="User's email address") 

217 ``` 

218 """ 

219 field_kwargs: dict[str, Any] = {} 

220 

221 if name: 

222 field_kwargs["name"] = name 

223 if description: 

224 field_kwargs["description"] = description 

225 if deprecation_reason: 

226 field_kwargs["deprecation_reason"] = deprecation_reason 

227 if default is not strawberry.UNSET: 

228 field_kwargs["default"] = default 

229 if default_factory: 

230 field_kwargs["default_factory"] = default_factory 

231 if permission_classes: 

232 field_kwargs["permission_classes"] = permission_classes 

233 

234 return strawberry.field(**field_kwargs) 

235 

236 

237def resolver( 

238 name: str | None = None, 

239 description: str | None = None, 

240) -> Callable[[Callable[P, T]], Callable[P, Any]]: 

241 """Decorator for field resolvers. 

242 

243 Use this to define a resolver function that can be used 

244 as a field resolver in a GraphQL type. 

245 

246 Args: 

247 name: Optional resolver name. 

248 description: ResolverProtocol description. 

249 

250 Returns: 

251 Decorated resolver function. 

252 

253 Example: 

254 ```python 

255 @resolver(description="Resolve user's full name") 

256 async def resolve_full_name( 

257 root: User, info: Info 

258 ) -> str: 

259 return f"{root.first_name} {root.last_name}" 

260 

261 @strawberry.type 

262 class User: 

263 first_name: str 

264 last_name: str 

265 full_name: str = strawberry.field(resolver=resolve_full_name) 

266 ``` 

267 """ 

268 

269 def decorator(func: Callable[P, T]) -> Callable[P, Any]: 

270 @functools.wraps(func) 

271 async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: 

272 # Support both sync and async resolver functions. 

273 result = func(*args, **kwargs) 

274 if isinstance(result, Awaitable): 

275 return cast("T", await result) 

276 return result 

277 

278 # Store metadata (use cast to avoid mypy attribute checks on callables) 

279 cast("Any", wrapper).__resolver_name__ = name or func.__name__ 

280 cast("Any", wrapper).__resolver_description__ = description 

281 

282 return wrapper 

283 

284 return decorator 

285 

286 

287def get_context(info: Info) -> GraphQLContext: 

288 """Get the GraphQL context from resolver info. 

289 

290 Args: 

291 info: Strawberry resolver info. 

292 

293 Returns: 

294 GraphQL context. 

295 

296 Example: 

297 ```python 

298 @strawberry.type 

299 class Query: 

300 @strawberry.field 

301 async def me(self, info: Info) -> User: 

302 context = get_context(info) 

303 return context.user 

304 ``` 

305 """ 

306 return cast("GraphQLContext", info.context) 

307 

308 

309__all__ = [ 

310 "field", 

311 "get_context", 

312 "mutation", 

313 "query", 

314 "resolver", 

315 "subscription", 

316]