Skip to content

Commit 74b55e4

Browse files
kurodo3[bot]claude
andcommitted
test(function-pod): update SEF tests to use unified FunctionPod.from_fn API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4684052 commit 74b55e4

1 file changed

Lines changed: 69 additions & 70 deletions

File tree

tests/test_core/side_effect_function/test_side_effect_function_pod.py

Lines changed: 69 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def _make_stream(n: int = 3) -> ArrowTableStream:
2121
return ArrowTableStream(table, tag_columns=["id"])
2222

2323

24-
def _make_in_memory_db():
24+
def _make_pipeline_db():
2525
"""Return a fresh in-memory ArrowDatabase."""
2626
from orcapod.databases.in_memory_databases import InMemoryArrowDatabase
2727
return InMemoryArrowDatabase()
@@ -32,69 +32,68 @@ class TestSideEffectFunctionPodSchema:
3232

3333
def test_sf01_ctx_stripped_from_input_schema(self):
3434
"""SF-01: 'ctx' param stripped; data params form the input schema."""
35-
from orcapod.core.side_effect_function import SideEffectFunctionPod
35+
from orcapod.core.function_pod import FunctionPod
3636

3737
def my_fn(value: int, ctx: InvocationContext) -> str:
3838
return f"result_{value}"
3939

40-
pod = SideEffectFunctionPod(my_fn, output_keys=["result"])
40+
pod = FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="ctx")
4141

4242
# Input schema excludes 'ctx'
43-
assert "ctx" not in pod.input_data_schema
44-
assert "value" in pod.input_data_schema
45-
assert pod.input_data_schema["value"] == int
43+
assert "ctx" not in pod._data_function.input_data_schema
44+
assert "value" in pod._data_function.input_data_schema
45+
assert pod._data_function.input_data_schema["value"] == int
4646

4747
# Output schema has the declared key
48-
assert "result" in pod.output_data_schema
49-
assert pod.output_data_schema["result"] == str
48+
assert "result" in pod._data_function.output_data_schema
49+
assert pod._data_function.output_data_schema["result"] == str
5050

5151
def test_sf02_custom_ctx_arg_name(self):
5252
"""SF-02: ctx_arg_name='context' — stripped and injected by correct name."""
53-
from orcapod.core.side_effect_function import SideEffectFunctionPod
53+
from orcapod.core.function_pod import FunctionPod
5454

5555
def my_fn(value: int, context: InvocationContext) -> str:
5656
return f"r_{value}"
5757

58-
pod = SideEffectFunctionPod(my_fn, output_keys=["result"], ctx_arg_name="context")
59-
assert "context" not in pod.input_data_schema
60-
assert "value" in pod.input_data_schema
58+
pod = FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="context")
59+
assert "context" not in pod._data_function.input_data_schema
60+
assert "value" in pod._data_function.input_data_schema
6161

6262
def test_sf03_missing_ctx_arg_raises_at_construction(self):
6363
"""SF-03: Missing ctx_arg_name raises ValueError at construction time."""
64-
from orcapod.core.side_effect_function import SideEffectFunctionPod
64+
from orcapod.core.function_pod import FunctionPod
6565

6666
def my_fn(value: int) -> str:
6767
return str(value)
6868

6969
with pytest.raises(ValueError, match="ctx_arg_name"):
70-
SideEffectFunctionPod(my_fn, output_keys=["result"])
71-
# Default ctx_arg_name="ctx" is missing from my_fn's signature
70+
FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="ctx")
7271

7372
def test_sf10_node_uri_shape(self):
74-
"""SF-10: uri[0]=='side_effect_function', uri[1:]==data_function.uri, len==5."""
75-
from orcapod.core.side_effect_function import SideEffectFunctionPod
73+
"""SF-10: uri[0]=='side_effect_function', uri[-1]=='python.function.v0', len==5."""
74+
from orcapod.core.function_pod import FunctionPod
7675

7776
def my_fn(value: int, ctx: InvocationContext) -> str:
7877
return str(value)
7978

80-
pod = SideEffectFunctionPod(my_fn, output_keys=["result"])
79+
pod = FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="ctx")
8180
assert pod.uri[0] == "side_effect_function"
8281
assert pod.uri[-1] == "python.function.v0"
8382
assert len(pod.uri) == 5
8483
assert pod.uri[3] == "v1"
8584

8685

8786
class TestSideEffectFunctionPodStreamStandalone:
88-
"""SF-04, SF-05: standalone execution via SideEffectFunctionPodStream."""
87+
"""SF-04, SF-05: standalone execution via FunctionPodStream."""
8988

9089
def test_sf04_iter_data_returns_correct_output(self):
9190
"""SF-04: iter_data() returns correct (tag, output_data) per row."""
92-
from orcapod.core.side_effect_function import SideEffectFunctionPod
91+
from orcapod.core.function_pod import FunctionPod
9392

9493
def my_fn(value: int, ctx: InvocationContext) -> str:
9594
return f"v{value}"
9695

97-
pod = SideEffectFunctionPod(my_fn, output_keys=["result"])
96+
pod = FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="ctx")
9897
stream = _make_stream(3)
9998
rows = list(pod.process(stream).iter_data())
10099

@@ -107,15 +106,15 @@ def my_fn(value: int, ctx: InvocationContext) -> str:
107106

108107
def test_sf05_invocation_context_fields_standalone(self):
109108
"""SF-05: InvocationContext has pod_name, non-empty hash, pipeline_run_id=None."""
110-
from orcapod.core.side_effect_function import SideEffectFunctionPod
109+
from orcapod.core.function_pod import FunctionPod
111110

112111
received_ctx: list[InvocationContext] = []
113112

114113
def my_fn(value: int, ctx: InvocationContext) -> str:
115114
received_ctx.append(ctx)
116115
return str(value)
117116

118-
pod = SideEffectFunctionPod(my_fn, output_keys=["result"])
117+
pod = FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="ctx")
119118
stream = _make_stream(1)
120119
list(pod.process(stream).iter_data())
121120

@@ -129,15 +128,15 @@ def my_fn(value: int, ctx: InvocationContext) -> str:
129128

130129
def test_sf05b_async_fn_routed_through_sync_execute(self):
131130
"""SF-05b: async user function executed correctly via _call_async_sync."""
132-
from orcapod.core.side_effect_function import SideEffectFunctionPod
131+
from orcapod.core.function_pod import FunctionPod
133132

134133
import asyncio
135134

136135
async def my_async_fn(value: int, ctx: InvocationContext) -> str:
137-
await asyncio.sleep(0) # yield control to ensure coroutine runs correctly
136+
await asyncio.sleep(0)
138137
return f"async_{value}"
139138

140-
pod = SideEffectFunctionPod(my_async_fn, output_keys=["result"])
139+
pod = FunctionPod.from_fn(my_async_fn, output_keys=["result"], ctx_arg_name="ctx")
141140
stream = _make_stream(2)
142141
rows = list(pod.process(stream).iter_data())
143142

@@ -147,11 +146,12 @@ async def my_async_fn(value: int, ctx: InvocationContext) -> str:
147146

148147

149148
class TestSideEffectFunctionJobNode:
150-
"""SF-06, SF-07, SF-09: DB-backed sync execution."""
149+
"""SF-06, SF-07, SF-09: DB-backed sync execution via FunctionJobNode."""
151150

152151
def test_sf06_output_cached_after_first_run(self):
153152
"""SF-06: Output cached; second run returns cached result without re-calling fn."""
154-
from orcapod.core.side_effect_function import SideEffectFunctionPod, SideEffectFunctionJobNode
153+
from orcapod.core.function_pod import FunctionPod
154+
from orcapod.core.nodes.function_node import FunctionJobNode
155155

156156
call_count = 0
157157

@@ -160,19 +160,19 @@ def my_fn(value: int, ctx: InvocationContext) -> str:
160160
call_count += 1
161161
return f"r{value}"
162162

163-
pod = SideEffectFunctionPod(my_fn, output_keys=["result"])
163+
pod = FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="ctx")
164164
stream = _make_stream(2)
165-
result_db = _make_in_memory_db()
165+
pipeline_db = _make_pipeline_db()
166166

167-
node1 = SideEffectFunctionJobNode(pod=pod, input_stream=stream)
168-
node1.attach_databases(result_database=result_db)
167+
node1 = FunctionJobNode(function_pod=pod, input_stream=stream)
168+
node1.attach_databases(pipeline_database=pipeline_db)
169169
results1 = node1.execute(stream)
170170
assert len(results1) == 2
171171
assert call_count == 2
172172

173-
# Second run — same pod, same data, same DBs — fn must NOT be called again
174-
node2 = SideEffectFunctionJobNode(pod=pod, input_stream=stream)
175-
node2.attach_databases(result_database=result_db)
173+
# Second run — same pod, same data, same DB — fn must NOT be called again
174+
node2 = FunctionJobNode(function_pod=pod, input_stream=stream)
175+
node2.attach_databases(pipeline_database=pipeline_db)
176176
results2 = node2.execute(stream)
177177
assert len(results2) == 2
178178
assert call_count == 2 # NOT incremented — cache hit
@@ -182,70 +182,68 @@ def my_fn(value: int, ctx: InvocationContext) -> str:
182182
assert d1.as_dict()["result"] == d2.as_dict()["result"]
183183

184184
def test_sf07_data_function_accessible_and_uri_consistent(self):
185-
"""SF-07: pod._data_function is a PythonDataFunction; uri = ('side_effect_function',) + data_function.uri."""
186-
from orcapod.core.side_effect_function import SideEffectFunctionPod
185+
"""SF-07: pod._data_function is a PythonDataFunction; uri starts with 'side_effect_function'."""
186+
from orcapod.core.function_pod import FunctionPod
187187
from orcapod.core.data_function import PythonDataFunction
188188

189189
def my_fn(value: int, ctx: InvocationContext) -> str:
190190
return f"r{value}"
191191

192-
pod = SideEffectFunctionPod(my_fn, output_keys=["result"])
192+
pod = FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="ctx")
193193
assert isinstance(pod._data_function, PythonDataFunction)
194194
assert pod.uri[0] == "side_effect_function"
195-
assert pod.uri[1:] == pod._data_function.uri
195+
assert pod._ctx_arg_name == "ctx"
196196

197-
def test_sf09_on_error_log_reraises(self):
198-
"""SF-09: on_error='log' — exception logged then always re-raised."""
199-
from orcapod.side_effects import SideEffectPodConfig
200-
from orcapod.core.side_effect_function import SideEffectFunctionPod, SideEffectFunctionJobNode
197+
def test_sf09_on_error_reraises(self):
198+
"""SF-09: exceptions from user function always propagate."""
199+
from orcapod.core.function_pod import FunctionPod
200+
from orcapod.core.nodes.function_node import FunctionJobNode
201201

202202
def my_fn(value: int, ctx: InvocationContext) -> str:
203203
raise RuntimeError("test error")
204204

205-
cfg = SideEffectPodConfig(on_error="log")
206-
pod = SideEffectFunctionPod(my_fn, output_keys=["result"], config=cfg)
205+
pod = FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="ctx")
207206
stream = _make_stream(1)
208-
result_db = _make_in_memory_db()
209-
node = SideEffectFunctionJobNode(pod=pod, input_stream=stream)
210-
node.attach_databases(result_database=result_db)
207+
pipeline_db = _make_pipeline_db()
208+
node = FunctionJobNode(function_pod=pod, input_stream=stream)
209+
node.attach_databases(pipeline_database=pipeline_db)
211210

212211
# Must propagate — no silent row suppression
213212
with pytest.raises(RuntimeError, match="test error"):
214-
node.execute(stream)
213+
node.execute(stream, error_policy="fail_fast")
215214

216215

217216
class TestSideEffectFunctionPodDecorator:
218217
"""SF-11: @side_effect_function_pod decorator."""
219218

220219
def test_sf11_decorator_creates_correct_pod(self):
221-
"""SF-11: Decorator creates a SideEffectFunctionPod with correct URI."""
222-
from orcapod.core.side_effect_function import (
223-
SideEffectFunctionPod,
224-
side_effect_function_pod,
225-
)
220+
"""SF-11: Decorator creates a FunctionPod with correct URI."""
221+
from orcapod.core.function_pod import FunctionPod, side_effect_function_pod
226222

227223
@side_effect_function_pod(output_keys=["result"])
228224
def my_fn(value: int, ctx: InvocationContext) -> str:
229225
return str(value)
230226

231-
assert isinstance(my_fn, SideEffectFunctionPod)
227+
assert isinstance(my_fn, FunctionPod)
232228
assert my_fn.uri[0] == "side_effect_function"
233229
assert my_fn.canonical_function_name == "my_fn"
234230

235231
def test_sf11_decorator_accessible_from_public_api(self):
236-
"""SF-11: Decorator and pod class accessible from orcapod top-level."""
232+
"""SF-11: Decorator accessible from orcapod top-level; class removed."""
237233
import orcapod
238234
assert hasattr(orcapod, "side_effect_function_pod")
239-
assert hasattr(orcapod, "SideEffectFunctionPod")
235+
assert hasattr(orcapod, "FunctionPod")
236+
# SideEffectFunctionPod class is no longer exported (unified into FunctionPod)
237+
assert not hasattr(orcapod, "SideEffectFunctionPod")
240238

241239

242240
class TestSideEffectFunctionPodPipelineIntegration:
243241
"""SF-12: Full pipeline compilation and execution."""
244242

245243
def test_sf12_pipeline_compilation_and_execution(self):
246-
"""SF-12: SideEffectFunctionJobNode compiled, fn called, caching works."""
244+
"""SF-12: FunctionJobNode compiled for ctx-aware pod, fn called, ctx received."""
247245
from orcapod.pipeline.job import PipelineJob
248-
from orcapod.core.side_effect_function import SideEffectFunctionPod, SideEffectFunctionJobNode
246+
from orcapod.core.function_pod import FunctionPod
249247
from orcapod.core.sources.dict_source import DictSource
250248

251249
received_ctx: list[InvocationContext] = []
@@ -254,8 +252,8 @@ def transform(value: int, ctx: InvocationContext) -> str:
254252
received_ctx.append(ctx)
255253
return f"result_{value}"
256254

257-
pod = SideEffectFunctionPod(transform, output_keys=["result"])
258-
db = _make_in_memory_db()
255+
pod = FunctionPod.from_fn(transform, output_keys=["result"], ctx_arg_name="ctx")
256+
db = _make_pipeline_db()
259257

260258
with PipelineJob(name="test_sef", store=db) as job:
261259
source = DictSource(
@@ -278,7 +276,7 @@ def transform(value: int, ctx: InvocationContext) -> str:
278276
def test_sf12_second_pipeline_run_uses_cache(self):
279277
"""SF-12: Second pipeline run uses cached output; fn not called again."""
280278
from orcapod.pipeline.job import PipelineJob
281-
from orcapod.core.side_effect_function import SideEffectFunctionPod
279+
from orcapod.core.function_pod import FunctionPod
282280
from orcapod.core.sources.dict_source import DictSource
283281

284282
call_count = 0
@@ -288,8 +286,8 @@ def transform(value: int, ctx: InvocationContext) -> str:
288286
call_count += 1
289287
return f"r{value}"
290288

291-
pod = SideEffectFunctionPod(transform, output_keys=["result"])
292-
db = _make_in_memory_db()
289+
pod = FunctionPod.from_fn(transform, output_keys=["result"], ctx_arg_name="ctx")
290+
db = _make_pipeline_db()
293291
source_data = [{"id": 0, "value": 10}, {"id": 1, "value": 20}]
294292

295293
with PipelineJob(name="test_sef_cache", store=db) as job1:
@@ -311,7 +309,8 @@ class TestSideEffectFunctionJobNodeAsync:
311309
def test_sf13_async_execute_basic(self):
312310
"""SF-13: async_execute processes all rows, writes cache, returns correct output."""
313311
import asyncio
314-
from orcapod.core.side_effect_function import SideEffectFunctionPod, SideEffectFunctionJobNode
312+
from orcapod.core.function_pod import FunctionPod
313+
from orcapod.core.nodes.function_node import FunctionJobNode
315314
from orcapod.channels import Channel
316315

317316
call_count = 0
@@ -321,11 +320,11 @@ def my_fn(value: int, ctx: InvocationContext) -> str:
321320
call_count += 1
322321
return f"async_{value}"
323322

324-
pod = SideEffectFunctionPod(my_fn, output_keys=["result"])
323+
pod = FunctionPod.from_fn(my_fn, output_keys=["result"], ctx_arg_name="ctx")
325324
stream = _make_stream(3)
326-
result_db = _make_in_memory_db()
327-
node = SideEffectFunctionJobNode(pod=pod, input_stream=stream)
328-
node.attach_databases(result_database=result_db)
325+
pipeline_db = _make_pipeline_db()
326+
node = FunctionJobNode(function_pod=pod, input_stream=stream)
327+
node.attach_databases(pipeline_database=pipeline_db)
329328

330329
async def _run():
331330
ch_in = Channel(buffer_size=10)
@@ -339,7 +338,7 @@ async def feed():
339338
await asyncio.gather(
340339
feed(),
341340
node.async_execute(
342-
[ch_in.reader], ch_out.writer, run_id="test-run-sf13"
341+
ch_in.reader, ch_out.writer, run_id="test-run-sf13"
343342
),
344343
)
345344
return await ch_out.reader.collect()

0 commit comments

Comments
 (0)