Skip to content

Commit 2cbb8f7

Browse files
smparkesclaudeanuraaga
authored
Generator: emit RequestContext[Input, Output] for service handlers (#264)
Signed-off-by: Steven Parkes <smparkes@smparkes.net> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Anuraag (Rag) Agrawal <anuraaga@gmail.com>
1 parent 0f4eb98 commit 2cbb8f7

5 files changed

Lines changed: 177 additions & 32 deletions

File tree

conformance/test/gen/connectrpc/conformance/v1/service_connect.py

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,20 @@ class ConformanceService(Protocol):
3737
async def unary(
3838
self,
3939
request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest,
40-
ctx: RequestContext,
40+
ctx: RequestContext[
41+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest,
42+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse,
43+
],
4144
) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse:
4245
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
4346

4447
def server_stream(
4548
self,
4649
request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest,
47-
ctx: RequestContext,
50+
ctx: RequestContext[
51+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest,
52+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse,
53+
],
4854
) -> AsyncIterator[
4955
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse
5056
]:
@@ -55,7 +61,10 @@ async def client_stream(
5561
request: AsyncIterator[
5662
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest
5763
],
58-
ctx: RequestContext,
64+
ctx: RequestContext[
65+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest,
66+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse,
67+
],
5968
) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse:
6069
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
6170

@@ -64,7 +73,10 @@ def bidi_stream(
6473
request: AsyncIterator[
6574
connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest
6675
],
67-
ctx: RequestContext,
76+
ctx: RequestContext[
77+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest,
78+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse,
79+
],
6880
) -> AsyncIterator[
6981
connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse
7082
]:
@@ -73,14 +85,20 @@ def bidi_stream(
7385
async def unimplemented(
7486
self,
7587
request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest,
76-
ctx: RequestContext,
88+
ctx: RequestContext[
89+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest,
90+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse,
91+
],
7792
) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse:
7893
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
7994

8095
async def idempotent_unary(
8196
self,
8297
request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest,
83-
ctx: RequestContext,
98+
ctx: RequestContext[
99+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest,
100+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse,
101+
],
84102
) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse:
85103
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
86104

@@ -307,14 +325,20 @@ class ConformanceServiceSync(Protocol):
307325
def unary(
308326
self,
309327
request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest,
310-
ctx: RequestContext,
328+
ctx: RequestContext[
329+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest,
330+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse,
331+
],
311332
) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse:
312333
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
313334

314335
def server_stream(
315336
self,
316337
request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest,
317-
ctx: RequestContext,
338+
ctx: RequestContext[
339+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest,
340+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse,
341+
],
318342
) -> Iterator[
319343
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse
320344
]:
@@ -325,7 +349,10 @@ def client_stream(
325349
request: Iterator[
326350
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest
327351
],
328-
ctx: RequestContext,
352+
ctx: RequestContext[
353+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest,
354+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse,
355+
],
329356
) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse:
330357
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
331358

@@ -334,7 +361,10 @@ def bidi_stream(
334361
request: Iterator[
335362
connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest
336363
],
337-
ctx: RequestContext,
364+
ctx: RequestContext[
365+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest,
366+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse,
367+
],
338368
) -> Iterator[
339369
connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse
340370
]:
@@ -343,14 +373,20 @@ def bidi_stream(
343373
def unimplemented(
344374
self,
345375
request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest,
346-
ctx: RequestContext,
376+
ctx: RequestContext[
377+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest,
378+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse,
379+
],
347380
) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse:
348381
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
349382

350383
def idempotent_unary(
351384
self,
352385
request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest,
353-
ctx: RequestContext,
386+
ctx: RequestContext[
387+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest,
388+
connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse,
389+
],
354390
) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse:
355391
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
356392

example/example/eliza_connect.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,31 @@
3535

3636
class ElizaService(Protocol):
3737
async def say(
38-
self, request: example_dot_eliza__pb2.SayRequest, ctx: RequestContext
38+
self,
39+
request: example_dot_eliza__pb2.SayRequest,
40+
ctx: RequestContext[
41+
example_dot_eliza__pb2.SayRequest, example_dot_eliza__pb2.SayResponse
42+
],
3943
) -> example_dot_eliza__pb2.SayResponse:
4044
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
4145

4246
def converse(
4347
self,
4448
request: AsyncIterator[example_dot_eliza__pb2.ConverseRequest],
45-
ctx: RequestContext,
49+
ctx: RequestContext[
50+
example_dot_eliza__pb2.ConverseRequest,
51+
example_dot_eliza__pb2.ConverseResponse,
52+
],
4653
) -> AsyncIterator[example_dot_eliza__pb2.ConverseResponse]:
4754
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
4855

4956
def introduce(
50-
self, request: example_dot_eliza__pb2.IntroduceRequest, ctx: RequestContext
57+
self,
58+
request: example_dot_eliza__pb2.IntroduceRequest,
59+
ctx: RequestContext[
60+
example_dot_eliza__pb2.IntroduceRequest,
61+
example_dot_eliza__pb2.IntroduceResponse,
62+
],
5163
) -> AsyncIterator[example_dot_eliza__pb2.IntroduceResponse]:
5264
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
5365

@@ -174,19 +186,31 @@ def introduce(
174186

175187
class ElizaServiceSync(Protocol):
176188
def say(
177-
self, request: example_dot_eliza__pb2.SayRequest, ctx: RequestContext
189+
self,
190+
request: example_dot_eliza__pb2.SayRequest,
191+
ctx: RequestContext[
192+
example_dot_eliza__pb2.SayRequest, example_dot_eliza__pb2.SayResponse
193+
],
178194
) -> example_dot_eliza__pb2.SayResponse:
179195
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
180196

181197
def converse(
182198
self,
183199
request: Iterator[example_dot_eliza__pb2.ConverseRequest],
184-
ctx: RequestContext,
200+
ctx: RequestContext[
201+
example_dot_eliza__pb2.ConverseRequest,
202+
example_dot_eliza__pb2.ConverseResponse,
203+
],
185204
) -> Iterator[example_dot_eliza__pb2.ConverseResponse]:
186205
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
187206

188207
def introduce(
189-
self, request: example_dot_eliza__pb2.IntroduceRequest, ctx: RequestContext
208+
self,
209+
request: example_dot_eliza__pb2.IntroduceRequest,
210+
ctx: RequestContext[
211+
example_dot_eliza__pb2.IntroduceRequest,
212+
example_dot_eliza__pb2.IntroduceResponse,
213+
],
190214
) -> Iterator[example_dot_eliza__pb2.IntroduceResponse]:
191215
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
192216

protoc-gen-connect-python/generator/template.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ from connectrpc.server import ConnectASGIApplication, ConnectWSGIApplication, En
6767
{{if not .SkipAsync }}
6868
{{- range .Services}}
6969
class {{.Name}}(Protocol):{{- range .Methods }}
70-
{{if not .ResponseStream }}async {{end}}def {{.PythonName}}(self, request: {{if .RequestStream}}AsyncIterator[{{end}}{{.InputType}}{{if .RequestStream}}]{{end}}, ctx: RequestContext) -> {{if .ResponseStream}}AsyncIterator[{{end}}{{.OutputType}}{{if .ResponseStream}}]{{end}}:
70+
{{if not .ResponseStream }}async {{end}}def {{.PythonName}}(self, request: {{if .RequestStream}}AsyncIterator[{{end}}{{.InputType}}{{if .RequestStream}}]{{end}}, ctx: RequestContext[{{.InputType}}, {{.OutputType}}]) -> {{if .ResponseStream}}AsyncIterator[{{end}}{{.OutputType}}{{if .ResponseStream}}]{{end}}:
7171
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
7272
{{ end }}
7373
@@ -131,7 +131,7 @@ class {{.Name}}Client(ConnectClient):{{range .Methods}}
131131
{{if not .SkipSync }}
132132
{{range .Services}}
133133
class {{.Name}}Sync(Protocol):{{- range .Methods }}
134-
def {{.PythonName}}(self, request: {{if .RequestStream}}Iterator[{{end}}{{.InputType}}{{if .RequestStream}}]{{end}}, ctx: RequestContext) -> {{if .ResponseStream}}Iterator[{{end}}{{.OutputType}}{{if .ResponseStream}}]{{end}}:
134+
def {{.PythonName}}(self, request: {{if .RequestStream}}Iterator[{{end}}{{.InputType}}{{if .RequestStream}}]{{end}}, ctx: RequestContext[{{.InputType}}, {{.OutputType}}]) -> {{if .ResponseStream}}Iterator[{{end}}{{.OutputType}}{{if .ResponseStream}}]{{end}}:
135135
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
136136
{{- end }}
137137

protoc-gen-connect-python/generator/template_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,54 @@ func TestConnectTemplate(t *testing.T) {
9090
})
9191
}
9292
}
93+
94+
func TestConnectTemplateRequestContextTypeParams(t *testing.T) {
95+
t.Parallel()
96+
97+
vars := ConnectTemplateVariables{
98+
FileName: "test.proto",
99+
ModuleName: "test",
100+
Services: []*ConnectService{
101+
{
102+
Package: "test",
103+
Name: "TestService",
104+
Methods: []*ConnectMethod{
105+
{
106+
Package: "test",
107+
ServiceName: "TestService",
108+
Name: "Unary",
109+
PythonName: "Unary",
110+
InputType: "_pb2.TestRequest",
111+
OutputType: "_pb2.TestResponse",
112+
},
113+
{
114+
Package: "test",
115+
ServiceName: "TestService",
116+
Name: "Bidi",
117+
PythonName: "Bidi",
118+
InputType: "_pb2.StreamRequest",
119+
OutputType: "_pb2.StreamResponse",
120+
Stream: true,
121+
RequestStream: true,
122+
ResponseStream: true,
123+
},
124+
},
125+
},
126+
},
127+
}
128+
129+
var buf bytes.Buffer
130+
if err := ConnectTemplate.Execute(&buf, vars); err != nil {
131+
t.Fatalf("Template execution failed: %v", err)
132+
}
133+
result := buf.String()
134+
135+
for _, want := range []string{
136+
"ctx: RequestContext[_pb2.TestRequest, _pb2.TestResponse]",
137+
"ctx: RequestContext[_pb2.StreamRequest, _pb2.StreamResponse]",
138+
} {
139+
if !strings.Contains(result, want) {
140+
t.Errorf("generated handler missing parameterized context %q\n--- got ---\n%s", want, result)
141+
}
142+
}
143+
}

test/haberdasher_connect.py

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,32 +37,49 @@
3737

3838
class Haberdasher(Protocol):
3939
async def make_hat(
40-
self, request: haberdasher__pb2.Size, ctx: RequestContext
40+
self,
41+
request: haberdasher__pb2.Size,
42+
ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat],
4143
) -> haberdasher__pb2.Hat:
4244
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
4345

4446
async def make_flexible_hat(
45-
self, request: AsyncIterator[haberdasher__pb2.Size], ctx: RequestContext
47+
self,
48+
request: AsyncIterator[haberdasher__pb2.Size],
49+
ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat],
4650
) -> haberdasher__pb2.Hat:
4751
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
4852

4953
def make_similar_hats(
50-
self, request: haberdasher__pb2.Size, ctx: RequestContext
54+
self,
55+
request: haberdasher__pb2.Size,
56+
ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat],
5157
) -> AsyncIterator[haberdasher__pb2.Hat]:
5258
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
5359

5460
def make_various_hats(
55-
self, request: AsyncIterator[haberdasher__pb2.Size], ctx: RequestContext
61+
self,
62+
request: AsyncIterator[haberdasher__pb2.Size],
63+
ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat],
5664
) -> AsyncIterator[haberdasher__pb2.Hat]:
5765
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
5866

5967
def list_parts(
60-
self, request: google_dot_protobuf_dot_empty__pb2.Empty, ctx: RequestContext
68+
self,
69+
request: google_dot_protobuf_dot_empty__pb2.Empty,
70+
ctx: RequestContext[
71+
google_dot_protobuf_dot_empty__pb2.Empty, haberdasher__pb2.Hat.Part
72+
],
6173
) -> AsyncIterator[haberdasher__pb2.Hat.Part]:
6274
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
6375

6476
async def do_nothing(
65-
self, request: google_dot_protobuf_dot_empty__pb2.Empty, ctx: RequestContext
77+
self,
78+
request: google_dot_protobuf_dot_empty__pb2.Empty,
79+
ctx: RequestContext[
80+
google_dot_protobuf_dot_empty__pb2.Empty,
81+
google_dot_protobuf_dot_empty__pb2.Empty,
82+
],
6683
) -> google_dot_protobuf_dot_empty__pb2.Empty:
6784
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
6885

@@ -279,32 +296,49 @@ async def do_nothing(
279296

280297
class HaberdasherSync(Protocol):
281298
def make_hat(
282-
self, request: haberdasher__pb2.Size, ctx: RequestContext
299+
self,
300+
request: haberdasher__pb2.Size,
301+
ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat],
283302
) -> haberdasher__pb2.Hat:
284303
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
285304

286305
def make_flexible_hat(
287-
self, request: Iterator[haberdasher__pb2.Size], ctx: RequestContext
306+
self,
307+
request: Iterator[haberdasher__pb2.Size],
308+
ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat],
288309
) -> haberdasher__pb2.Hat:
289310
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
290311

291312
def make_similar_hats(
292-
self, request: haberdasher__pb2.Size, ctx: RequestContext
313+
self,
314+
request: haberdasher__pb2.Size,
315+
ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat],
293316
) -> Iterator[haberdasher__pb2.Hat]:
294317
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
295318

296319
def make_various_hats(
297-
self, request: Iterator[haberdasher__pb2.Size], ctx: RequestContext
320+
self,
321+
request: Iterator[haberdasher__pb2.Size],
322+
ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat],
298323
) -> Iterator[haberdasher__pb2.Hat]:
299324
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
300325

301326
def list_parts(
302-
self, request: google_dot_protobuf_dot_empty__pb2.Empty, ctx: RequestContext
327+
self,
328+
request: google_dot_protobuf_dot_empty__pb2.Empty,
329+
ctx: RequestContext[
330+
google_dot_protobuf_dot_empty__pb2.Empty, haberdasher__pb2.Hat.Part
331+
],
303332
) -> Iterator[haberdasher__pb2.Hat.Part]:
304333
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
305334

306335
def do_nothing(
307-
self, request: google_dot_protobuf_dot_empty__pb2.Empty, ctx: RequestContext
336+
self,
337+
request: google_dot_protobuf_dot_empty__pb2.Empty,
338+
ctx: RequestContext[
339+
google_dot_protobuf_dot_empty__pb2.Empty,
340+
google_dot_protobuf_dot_empty__pb2.Empty,
341+
],
308342
) -> google_dot_protobuf_dot_empty__pb2.Empty:
309343
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
310344

0 commit comments

Comments
 (0)