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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""GraphQL schema decorators.
3This module provides decorator functions for defining GraphQL
4resolvers, queries, mutations, and subscriptions using Strawberry.
5"""
7from __future__ import annotations
9from collections.abc import Awaitable, Callable
10import functools
11from typing import (
12 TYPE_CHECKING,
13 Any,
14 ParamSpec,
15 TypeVar,
16 cast,
17)
19import strawberry
20from strawberry import Info
22from lexigram.logging import get_logger
24if TYPE_CHECKING:
25 from lexigram.graphql.core.context import GraphQLContext
27logger = get_logger(__name__)
29P = ParamSpec("P")
30T = TypeVar("T")
31R = TypeVar("R")
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.
42 This is a wrapper around Strawberry's field decorator with
43 additional Lexigram-specific functionality.
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.
51 Returns:
52 Decorated function.
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 """
64 def decorator(func: Callable[P, T]) -> Callable[P, T]:
65 # Build kwargs for strawberry.field
66 field_kwargs: dict[str, Any] = {}
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
77 # Apply strawberry field decorator
78 return cast("Callable[P, T]", strawberry.field(**field_kwargs)(func))
80 return decorator
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.
91 This is a wrapper around Strawberry's mutation decorator with
92 additional Lexigram-specific functionality.
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.
100 Returns:
101 Decorated function.
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 """
115 def decorator(func: Callable[P, T]) -> Callable[P, T]:
116 # Build kwargs for strawberry.mutation
117 mutation_kwargs: dict[str, Any] = {}
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
128 # Apply strawberry mutation decorator
129 return cast("Callable[P, T]", strawberry.mutation(**mutation_kwargs)(func))
131 return decorator
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.
142 This is a wrapper around Strawberry's subscription decorator with
143 additional Lexigram-specific functionality.
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.
151 Returns:
152 Decorated function.
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 """
167 def decorator(func: Callable[P, T]) -> Callable[P, T]:
168 # Build kwargs for strawberry.subscription
169 sub_kwargs: dict[str, Any] = {}
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
180 # Apply strawberry subscription decorator
181 return cast("Callable[P, T]", strawberry.subscription(**sub_kwargs)(func))
183 return decorator
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.
196 This is a wrapper around Strawberry's field function with
197 additional Lexigram-specific functionality.
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.
207 Returns:
208 Strawberry field.
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] = {}
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
234 return strawberry.field(**field_kwargs)
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.
243 Use this to define a resolver function that can be used
244 as a field resolver in a GraphQL type.
246 Args:
247 name: Optional resolver name.
248 description: ResolverProtocol description.
250 Returns:
251 Decorated resolver function.
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}"
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 """
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
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
282 return wrapper
284 return decorator
287def get_context(info: Info) -> GraphQLContext:
288 """Get the GraphQL context from resolver info.
290 Args:
291 info: Strawberry resolver info.
293 Returns:
294 GraphQL context.
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)
309__all__ = [
310 "field",
311 "get_context",
312 "mutation",
313 "query",
314 "resolver",
315 "subscription",
316]