From b1fcca0a1c067d608d34f6f99358a5770969886c Mon Sep 17 00:00:00 2001 From: proboscis Date: Sat, 13 Jun 2026 00:01:16 +0900 Subject: [PATCH] Delete public WithHandler shim --- .semgrep.yaml | 30 ++++++ benchmarks/benchmark_runner.py | 4 +- doeff/__init__.py | 8 +- doeff/cli/run_services.py | 6 +- doeff/handler_utils.py | 2 +- doeff/program.py | 76 ++++++--------- .../doeff-agentic/examples/01_hello_agent.py | 10 +- .../examples/02_agent_with_status.py | 10 +- .../examples/03_sequential_agents.py | 10 +- .../examples/04_conditional_flow.py | 10 +- .../examples/05_human_in_loop.py | 10 +- .../examples/06_parallel_agents.py | 10 +- .../examples/07_pr_review_workflow.py | 10 +- .../examples/08_testing_with_mocks.py | 4 +- .../src/doeff_agentic/handlers/opencode.py | 13 ++- .../src/doeff_agentic/handlers/production.py | 14 ++- .../src/doeff_agentic/handlers/testing.py | 5 +- .../src/doeff_agentic/handlers/tmux.py | 13 ++- .../src/doeff_agentic/runtime.py | 4 +- .../tests/test_handlers_module.py | 11 +-- .../tests/test_visual_interceptor.py | 7 +- packages/doeff-agents/examples/_runtime.py | 5 +- .../src/doeff_agents/handlers/__init__.py | 11 ++- .../src/doeff_agents/handlers/effectful.hy | 4 +- .../doeff_agents/handlers/mcp_server_loop.hy | 3 +- .../src/doeff_agents/handlers/testing.py | 5 +- .../tests/test_agent_session_state.py | 5 +- .../doeff-agents/tests/test_claude_handler.py | 4 +- .../tests/test_claude_integration.py | 19 +--- .../doeff-agents/tests/test_codex_handler.py | 12 +-- .../tests/test_handler_protocol.py | 12 +-- packages/doeff-agents/tests/test_mcp_e2e.py | 10 +- .../doeff-agents/tests/test_mcp_live_e2e.py | 4 +- .../tests/test_mixed_agent_handlers.py | 10 +- .../tests/test_session_backend.py | 7 +- .../test_withhandler_agent_delegation.py | 36 +++---- .../src/doeff_conductor/api.py | 4 +- .../src/doeff_conductor/handlers/__init__.py | 7 +- .../src/doeff_conductor/handlers/testing.py | 7 +- .../src/doeff_conductor/handlers/utils.py | 3 +- .../tests/test_k4_parallel_overlap.py | 11 ++- .../doeff_core_effects/cache_handlers.py | 5 +- .../doeff_core_effects/handlers.py | 87 +++++++---------- .../doeff_core_effects/memo_handlers.py | 12 +-- .../doeff_core_effects/scheduler.py | 15 +-- .../doeff-docker/src/doeff_docker/compose.hy | 7 +- packages/doeff-docker/tests/test_effects.py | 7 +- .../src/doeff_events/handlers/memory.py | 3 +- .../tests/test_multiple_listeners.py | 6 +- packages/doeff-events/tests/test_publish.py | 6 +- .../doeff-events/tests/test_typed_dispatch.py | 6 +- .../doeff-events/tests/test_wait_for_event.py | 4 +- .../examples/05_durable_execution.py | 3 +- .../examples/06_testing_with_mocks.py | 4 +- .../src/doeff_flow/handlers/production.py | 3 +- .../src/doeff_flow/handlers/testing.py | 3 +- packages/doeff-flow/tests/test_e2e.py | 7 +- .../doeff-flow/tests/test_effect_handlers.py | 4 +- packages/doeff-flow/tests/test_trace.py | 7 +- .../src/doeff_gemini/handlers/production.py | 3 +- .../src/doeff_gemini/handlers/testing.py | 3 +- .../tests/e2e/test_gemini_live.py | 5 +- .../tests/unit/test_effect_handlers.py | 6 +- .../tests/unit/test_structured_llm.py | 7 +- .../src/doeff_git/handlers/production.py | 3 +- .../src/doeff_git/handlers/testing.py | 3 +- .../tests/unit/test_effect_handlers.py | 4 +- .../handlers/production.py | 3 +- .../tests/unit/test_effect_handlers.py | 4 +- .../tests/unit/test_secrets.py | 18 ++-- packages/doeff-hy/src/doeff_hy/handle.hy | 22 ++--- packages/doeff-hy/tests/test_defpipeline.hy | 6 +- .../doeff-hy/tests/test_duplicate_stages.hy | 4 +- .../doeff-hy/tests/test_lazy_and_threading.py | 60 ++++++------ packages/doeff-hy/tests/test_multimod.hy | 6 +- packages/doeff-hy/tests/test_stage_of.hy | 5 +- .../tests/test_multi_provider_workflow.py | 17 +--- .../src/doeff_ml_nexus/runner.hy | 2 +- .../src/doeff_ml_nexus/stack.hy | 2 +- packages/doeff-ml-nexus/tests/test_docker.py | 7 +- packages/doeff-notify/tests/test_handlers.py | 10 +- .../src/doeff_openai/handlers/testing.py | 3 +- packages/doeff-openai/tests/_runner.py | 20 ++-- .../tests/e2e/test_effect_handlers_e2e.py | 6 +- .../doeff-openai/tests/e2e/test_openai_api.py | 10 +- .../tests/unit/test_effect_handlers.py | 6 +- .../doeff-openai/tests/unit/test_pricing.py | 16 ++- .../tests/unit/test_structured_llm.py | 4 +- .../src/doeff_openrouter/handlers/testing.py | 3 +- .../tests/e2e/test_openrouter_integration.py | 6 +- .../tests/unit/test_effect_handlers.py | 7 +- .../doeff-preset/examples/01_basic_slog.py | 4 +- .../doeff-preset/examples/02_configuration.py | 6 +- .../examples/03_merge_handlers.py | 14 +-- .../examples/04_granular_handlers.py | 8 +- .../doeff-preset/examples/05_async_runtime.py | 5 +- .../doeff-preset/src/doeff_preset/__init__.py | 4 +- .../src/doeff_preset/handlers/config.py | 3 +- .../src/doeff_preset/handlers/log_display.py | 3 +- .../src/doeff_preset/handlers/production.py | 3 +- .../src/doeff_preset/handlers/testing.py | 3 +- .../tests/test_preset_handlers.py | 14 ++- .../doeff-secret/src/doeff_secret/handlers.py | 5 +- .../doeff-secret/src/doeff_secret/testing.py | 3 +- .../tests/test_effect_handlers.py | 24 ++--- .../src/doeff_seedream/handlers/production.py | 3 +- .../src/doeff_seedream/handlers/testing.py | 3 +- .../doeff-seedream/tests/test_edit_image.py | 8 +- .../tests/test_effect_handlers.py | 4 +- .../doeff_test_target/handlers/production.py | 3 +- .../src/doeff_test_target/handlers/testing.py | 3 +- .../tests/test_effect_handlers.py | 4 +- .../tests/test_with_handler_patterns.py | 13 +-- .../src/doeff_time/handlers/async_time.py | 3 +- .../src/doeff_time/handlers/sync_time.py | 3 +- packages/doeff-time/tests/conftest.py | 20 ++-- .../doeff-time/tests/test_async_handler.py | 8 +- packages/doeff-time/tests/test_get_time.py | 4 +- packages/doeff-time/tests/test_schedule_at.py | 12 +-- .../tests/test_sim_daemon_pattern.py | 11 +-- .../tests/test_sim_migration_readiness.py | 27 ++---- packages/doeff-time/tests/test_sim_time.py | 30 ++---- .../doeff-time/tests/test_sync_handler.py | 8 +- .../doeff-traverse/doeff_traverse/handlers.py | 61 ++++++------ .../examples/ex01_basic_fail.hy | 13 +-- .../examples/ex02_traverse_pipeline.hy | 4 +- .../examples/ex03_zip_multi_source.hy | 5 +- .../doeff-traverse/examples/ex04_simple.hy | 7 +- .../examples/ex04_traverse_macro.hy | 4 +- .../examples/ex05_parallel_timing.hy | 4 +- .../examples/ex06_mediagen_rewrite.hy | 4 +- .../examples/ex07_proboscis_ema_rewrite.hy | 4 +- .../examples/ex08_tqdm_progress.hy | 4 +- .../examples/ex09_for_do_when.hy | 3 +- .../tests/test_memory_leak_multi_day.py | 3 +- .../tests/test_traverse_deep_recursion.py | 5 +- packages/doeff-vm/tests/test_memory_stats.py | 1 + tests/_run_helpers.py | 5 +- .../test_no_public_withhandler_shim.py | 51 ++++++++++ tests/cli/test_cli_error_traces.py | 8 +- tests/cli/test_cli_main.py | 4 +- tests/conftest.py | 5 +- tests/core/test_delegate_non_terminal.py | 20 ++-- .../core/test_get_execution_context_effect.py | 4 +- tests/core/test_kleisli_characterization.py | 24 ++--- tests/core/test_nested_do_spawn_gather.py | 10 +- tests/core/test_pass_primitive.py | 5 +- tests/core/test_runtime_regressions_manual.py | 4 +- .../core/test_spawn_gather_with_intercept.py | 6 +- tests/core/test_spawn_try_gather.py | 8 +- tests/core/test_spec_gaps.py | 12 +-- tests/core/test_unified_traceback_system.py | 5 +- tests/core/test_vm_architecture_ocaml5.py | 14 +-- .../test_vm_legacy_tag_parser_fail_loud.py | 2 +- tests/effects/http_request_deftest_cases.hy | 19 ++-- tests/effects/test_env_var_ask.py | 33 +++---- ...test_handler_bind_exception_propagation.py | 5 +- tests/effects/test_lazy_ask.py | 9 +- .../effects/test_lazy_ask_env_var_fallback.py | 43 ++------ ...test_lazy_ask_inner_handler_propagation.py | 73 ++------------ tests/effects/test_local_handler_ask.py | 9 +- tests/effects/test_memo_no_terminal.py | 5 +- .../test_memo_rewriter_compute_unhandled.py | 5 +- .../effects/test_memo_rewriter_no_terminal.py | 7 +- tests/effects/test_traverse.py | 10 +- tests/effects/test_unhandled_effect_chain.py | 18 +--- tests/effects/test_unhandled_effect_class.py | 9 +- tests/fixtures/repro_387.hy | 2 +- tests/fixtures/repro_387_no_require.hy | 2 +- tests/fixtures/repro_387_via_macros.hy | 2 +- tests/public_api/test_do_bang_defp_defpp.py | 16 +-- .../test_doeff13_hang_regression.py | 4 +- .../test_types_001_handler_protocol.py | 30 +++--- tests/public_api/test_types_001_validation.py | 36 +++---- tests/test_await_http_memory.py | 11 +-- tests/test_cache_await_spawn_hang.py | 4 +- tests/test_cli_env_merge_with_program.py | 4 +- tests/test_core_effects.py | 40 ++++---- tests/test_deep_handler.py | 11 ++- tests/test_deftest_macro.py | 7 +- tests/test_do_bang_setv.py | 7 +- tests/test_double_resume_traceback.py | 5 +- tests/test_error_propagation.py | 5 +- tests/test_error_traceback_consistency.py | 11 ++- tests/test_execution_scope.py | 4 +- tests/test_get_handlers_defk.py | 7 +- tests/test_handle_macro.py | 96 +++++------------- tests/test_handler_chain_traversal.py | 27 +++--- tests/test_handler_exception_catchable.py | 28 +++--- tests/test_handler_nested_do.py | 15 +-- tests/test_intercept.py | 14 +-- tests/test_llm_multi_provider_handlers.py | 9 +- tests/test_macro_self_contained.py | 13 +-- .../test_memo_rewriter_spawn_continuation.py | 4 +- tests/test_new_vm.py | 35 +++---- .../test_pass_dispatch_context_regression.py | 5 +- tests/test_pass_traceback.py | 7 +- tests/test_scheduled_handler_isolation.py | 21 ++-- tests/test_scheduler.py | 4 +- tests/test_segment_owned_state.py | 4 +- tests/test_spawn_with_handlers.py | 10 +- tests/test_try_deep_in_handler_body.py | 13 +-- tests/test_try_finally_in_do.py | 8 +- tests/test_try_handler.py | 6 +- tests/test_try_handler_scope.py | 13 +-- tests/test_try_in_handler_body.py | 7 +- tests/test_with_observe_visibility.py | 7 +- tests/test_withhandler_shim_deprecation.py | 97 ------------------- tests/test_withhandler_validation.py | 45 +++++---- 209 files changed, 1042 insertions(+), 1390 deletions(-) create mode 100644 tests/architecture/test_no_public_withhandler_shim.py delete mode 100644 tests/test_withhandler_shim_deprecation.py diff --git a/.semgrep.yaml b/.semgrep.yaml index e3a2058fb..c4e357906 100644 --- a/.semgrep.yaml +++ b/.semgrep.yaml @@ -2206,6 +2206,36 @@ rules: category: doexpr-hierarchy rationale: "return_clause was removed; post-process handler results via ordinary @do composition" + - id: doeff-no-public-withhandler-shim + pattern-either: + - pattern-regex: (?m)^\s*from\s+doeff\s+import\s+.*\bWithHandler\b + - pattern-regex: (?m)^\s*from\s+doeff\.program\s+import\s+.*\bWithHandler\b + - pattern-regex: (?m)^\s*\(import\s+doeff\s+\[[^\]]*\bWithHandler\b + - pattern-regex: (?m)^\s*\(import\s+doeff\.program\s+\[[^\]]*\bWithHandler\b + - pattern-regex: \bdoeff\.WithHandler\s*\( + - pattern-regex: \bprogram\.WithHandler\s*\( + paths: + include: + - "doeff/**/*.py" + - "packages/**/*.py" + - "packages/**/*.pyi" + - "packages/**/*.hy" + - "tests/**/*.py" + - "tests/**/*.hy" + - "examples/**/*.py" + - "examples/**/*.hy" + exclude: + - "tests/semgrep/fixtures/**" + message: | + Public WithHandler shim usage is banned (core-withhandler-compat-shim-delete). + Compose handlers by calling Program -> Program handlers directly: handler(program). + Deliberate VM-node tests must import WithHandler from doeff_vm explicitly. + languages: [generic] + severity: ERROR + metadata: + category: doexpr-hierarchy + rationale: "public WithHandler compatibility shim was deleted; h(body) is the only user idiom" + - id: no-legacy-intercept-api pattern-either: - pattern-regex: (?m)^from\s+doeff\s+import\s+.*\bIntercept\b diff --git a/benchmarks/benchmark_runner.py b/benchmarks/benchmark_runner.py index 23c0ecb3d..b2713dc54 100644 --- a/benchmarks/benchmark_runner.py +++ b/benchmarks/benchmark_runner.py @@ -37,7 +37,9 @@ from doeff_vm import EffectBase from doeff import Apply, Pass, Pure, Resume, do, run -from doeff.program import WithHandlerType as VMWithHandler +from doeff.program import ( + WithHandlerType as VMWithHandler, +) DEFAULT_RUNS = 20 DEFAULT_LOOP_ITERATIONS = 100 diff --git a/doeff/__init__.py b/doeff/__init__.py index c66f6b97e..551ce2aee 100644 --- a/doeff/__init__.py +++ b/doeff/__init__.py @@ -32,9 +32,11 @@ from doeff.program import ResumeThrow as ResumeThrow from doeff.program import Transfer as Transfer from doeff.program import TransferThrow as TransferThrow -from doeff.program import WithHandler as WithHandler -from doeff.program import WithHandlerType as WithHandlerType +from doeff.program import ( + WithHandlerType as WithHandlerType, +) from doeff.program import WithObserve as WithObserveRaw +from doeff.program import handler as handler from doeff.program import program as program from doeff.result import Err as Err from doeff.result import Maybe as Maybe @@ -116,7 +118,7 @@ class DoExpr(metaclass=_DoExprMeta): """Virtual base type for all doeff program nodes. isinstance(x, DoExpr) returns True for any program node - (Pure, Expand, WithHandler, etc.). + (Pure, Expand, WithHandlerType, etc.). """ diff --git a/doeff/cli/run_services.py b/doeff/cli/run_services.py index d7fe61244..6a5cc729b 100644 --- a/doeff/cli/run_services.py +++ b/doeff/cli/run_services.py @@ -8,6 +8,8 @@ from pathlib import Path from typing import Any +from doeff.program import handler as _program_handler + # Activate Hy's import hook so doeff run can resolve program/interpreter/env # paths that live in .hy files (e.g. nakagawa.live.agent_pipeline.p_agent_daily). # Without this, importlib.import_module for a .hy module silently creates an @@ -183,7 +185,7 @@ def default_interpreter(program: Any) -> Any: ) from doeff_core_effects.scheduler import scheduled - from doeff import WithHandler, run + from doeff import run handlers = [ lazy_ask(), state(), writer(), try_handler, slog_handler(), @@ -191,7 +193,7 @@ def default_interpreter(program: Any) -> Any: ] wrapped = program for h in reversed(handlers): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) return run(scheduled(wrapped)) diff --git a/doeff/handler_utils.py b/doeff/handler_utils.py index dc98a05e3..2ac9613cc 100644 --- a/doeff/handler_utils.py +++ b/doeff/handler_utils.py @@ -17,7 +17,7 @@ def get_inner_handlers(k): Usage in a handler: inner_hs = yield get_inner_handlers(k) for h in inner_hs: - prog = WithHandler(h, prog) + prog = handler(h)(prog) """ all_hs = yield GetHandlers(k) if all_hs: diff --git a/doeff/program.py b/doeff/program.py index a66e55492..d9ac3893c 100644 --- a/doeff/program.py +++ b/doeff/program.py @@ -4,6 +4,8 @@ The VM classifies them via downcast (not tag-based getattr). """ +from typing import Any, cast + from doeff_vm import Apply as Apply from doeff_vm import Expand as Expand from doeff_vm import GetExecutionContext as GetExecutionContext @@ -17,58 +19,34 @@ from doeff_vm import ResumeThrow as ResumeThrow from doeff_vm import Transfer as Transfer from doeff_vm import TransferThrow as TransferThrow -from doeff_vm import WithHandler as _WithHandlerNode +from doeff_vm import WithHandler as WithHandlerType from doeff_vm import WithObserve as WithObserve -WithHandlerType = _WithHandlerNode - - -_NEW_STYLE_DEPRECATION_MSG = ( - "WithHandler(h, body) is deprecated: h is already a Program -> Program " - "function produced by defhandler, call it directly as h(body) " - "(or in Hy: (h body)). The shim stays in place indefinitely for " - "backward compatibility but emits this warning to steer new code." -) - -_LEGACY_DEPRECATION_MSG = ( - "WithHandler(h, body) with a raw @do-dispatcher ``h`` is deprecated: " - "rewrite the handler with defhandler (Hy) or @handler-style factory " - "that returns a Program -> Program function, then call it as h(body). " - "The shim stays in place indefinitely for backward compatibility but " - "emits this warning to steer new code toward the PR A1 idiom." -) - - -def WithHandler(h, body, *args, **kwargs): # noqa: N802 - public compatibility constructor - """Install handler ``h`` around ``body`` — **deprecated**. - - New-style handlers built with ``defhandler`` are already Program → - Program functions; prefer calling them directly:: - - # Before - WithHandler(my_handler, program) - # After - my_handler(program) - - Accepts two forms for backward compatibility: - - - New-style: ``h`` is a function ``Program -> Program`` marked with - ``_doeff_is_handler_fn = True``. The call is forwarded as ``h(body)``. - Emits :class:`DeprecationWarning`. - - Legacy: ``h`` is a raw ``@do``-decorated dispatcher ``fn[effect, k]``. - Falls through to the Rust ``WithHandler`` pyclass. Emits - :class:`DeprecationWarning` pointing at ``defhandler``. - - The shim itself is permanent (scope A) — the warning is informational - and does not break existing code. - """ - import warnings - if getattr(h, "_doeff_is_handler_fn", False): - warnings.warn(_NEW_STYLE_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return h(body, *args, **kwargs) - warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) - return _WithHandlerNode(h, body, *args, **kwargs) +def handler(raw_handler): + """Wrap a raw effect dispatcher as a Program -> Program handler.""" + if not callable(raw_handler): + raise TypeError( + f"handler: raw_handler must be callable, got {type(raw_handler).__name__}" + ) + raw_handler_meta = cast(Any, raw_handler) + try: + is_handler_fn = raw_handler_meta._doeff_is_handler_fn + except AttributeError: + is_handler_fn = False + if is_handler_fn is True: + return raw_handler + + def install(body): + return WithHandlerType(raw_handler, body) + + install.__name__ = raw_handler_meta.__name__ + install.__qualname__ = raw_handler_meta.__qualname__ + install.__doc__ = raw_handler_meta.__doc__ + install_meta = cast(Any, install) + install_meta._doeff_is_handler_fn = True + install_meta.__doeff_handler_data__ = raw_handler + return install def program(gen_fn, *args): diff --git a/packages/doeff-agentic/examples/01_hello_agent.py b/packages/doeff-agentic/examples/01_hello_agent.py index c455f4eec..caa92e85f 100644 --- a/packages/doeff-agentic/examples/01_hello_agent.py +++ b/packages/doeff-agentic/examples/01_hello_agent.py @@ -48,7 +48,7 @@ def hello_agent(): if __name__ == "__main__": import asyncio - from doeff import WithHandler, async_run, default_handlers + from doeff import async_run, default_handlers async def main(): print("Starting hello_agent workflow...") @@ -58,13 +58,7 @@ async def main(): # Merge preset handlers with opencode handlers # Preset provides: slog display (WriterTellEffect) + config (Ask preset.*) # OpenCode provides: agent session management effects - program = WithHandler( - preset_handlers(), - WithHandler( - opencode_handler(), - hello_agent(), - ), - ) + program = preset_handlers()(opencode_handler()(hello_agent())) result = await async_run(program, handlers=default_handlers()) if result.is_err(): diff --git a/packages/doeff-agentic/examples/02_agent_with_status.py b/packages/doeff-agentic/examples/02_agent_with_status.py index f6aa08899..337dfa111 100644 --- a/packages/doeff-agentic/examples/02_agent_with_status.py +++ b/packages/doeff-agentic/examples/02_agent_with_status.py @@ -62,7 +62,7 @@ def agent_with_status(): if __name__ == "__main__": import asyncio - from doeff import WithHandler, async_run, default_handlers + from doeff import async_run, default_handlers async def main(): print("Starting agent_with_status workflow...") @@ -75,13 +75,7 @@ async def main(): # Merge preset handlers with opencode handlers # Preset provides: slog display (WriterTellEffect) + config (Ask preset.*) # OpenCode provides: agent session management effects - program = WithHandler( - preset_handlers(), - WithHandler( - opencode_handler(), - agent_with_status(), - ), - ) + program = preset_handlers()(opencode_handler()(agent_with_status())) result = await async_run(program, handlers=default_handlers()) if result.is_err(): diff --git a/packages/doeff-agentic/examples/03_sequential_agents.py b/packages/doeff-agentic/examples/03_sequential_agents.py index 9db61f23e..7e90e69f3 100644 --- a/packages/doeff-agentic/examples/03_sequential_agents.py +++ b/packages/doeff-agentic/examples/03_sequential_agents.py @@ -68,7 +68,7 @@ def research_and_summarize(topic: str): if __name__ == "__main__": import asyncio - from doeff import WithHandler, async_run, default_handlers + from doeff import async_run, default_handlers async def main(): topic = "functional programming" @@ -79,13 +79,7 @@ async def main(): # Merge preset handlers with opencode handlers # Preset provides: slog display (WriterTellEffect) + config (Ask preset.*) # OpenCode provides: agent session management effects - program = WithHandler( - preset_handlers(), - WithHandler( - opencode_handler(), - research_and_summarize(topic), - ), - ) + program = preset_handlers()(opencode_handler()(research_and_summarize(topic))) result = await async_run(program, handlers=default_handlers()) if result.is_err(): diff --git a/packages/doeff-agentic/examples/04_conditional_flow.py b/packages/doeff-agentic/examples/04_conditional_flow.py index 6aca25457..7141cf200 100644 --- a/packages/doeff-agentic/examples/04_conditional_flow.py +++ b/packages/doeff-agentic/examples/04_conditional_flow.py @@ -80,7 +80,7 @@ def review_and_maybe_fix(code: str): if __name__ == "__main__": import asyncio - from doeff import WithHandler, async_run, default_handlers + from doeff import async_run, default_handlers async def main(): # Code with issues to review @@ -101,13 +101,7 @@ def calculate_average(numbers): # Merge preset handlers with opencode handlers # Preset provides: slog display (WriterTellEffect) + config (Ask preset.*) # OpenCode provides: agent session management effects - program = WithHandler( - preset_handlers(), - WithHandler( - opencode_handler(), - review_and_maybe_fix(sample_code), - ), - ) + program = preset_handlers()(opencode_handler()(review_and_maybe_fix(sample_code))) result = await async_run(program, handlers=default_handlers()) if result.is_err(): diff --git a/packages/doeff-agentic/examples/05_human_in_loop.py b/packages/doeff-agentic/examples/05_human_in_loop.py index 2e285e8de..25cbfac8e 100644 --- a/packages/doeff-agentic/examples/05_human_in_loop.py +++ b/packages/doeff-agentic/examples/05_human_in_loop.py @@ -164,7 +164,7 @@ def draft_with_approval(task: str): if __name__ == "__main__": import asyncio - from doeff import WithHandler, async_run, default_handlers + from doeff import async_run, default_handlers async def main(): task = "Write a haiku about programming" @@ -176,13 +176,7 @@ async def main(): # Merge preset handlers with opencode handlers # Preset provides: slog display (WriterTellEffect) + config (Ask preset.*) # OpenCode provides: agent session management effects - program = WithHandler( - preset_handlers(), - WithHandler( - opencode_handler(), - draft_with_approval(task), - ), - ) + program = preset_handlers()(opencode_handler()(draft_with_approval(task))) result = await async_run(program, handlers=default_handlers()) if result.is_err(): diff --git a/packages/doeff-agentic/examples/06_parallel_agents.py b/packages/doeff-agentic/examples/06_parallel_agents.py index f2c4e2cd9..c0778abf4 100644 --- a/packages/doeff-agentic/examples/06_parallel_agents.py +++ b/packages/doeff-agentic/examples/06_parallel_agents.py @@ -116,7 +116,7 @@ def multi_perspective_analysis(topic: str): if __name__ == "__main__": import asyncio - from doeff import WithHandler, async_run, default_handlers + from doeff import async_run, default_handlers async def main(): topic = "AI code assistants" @@ -127,13 +127,7 @@ async def main(): # Merge preset handlers with opencode handlers # Preset provides: slog display (WriterTellEffect) + config (Ask preset.*) # OpenCode provides: agent session management effects - program = WithHandler( - preset_handlers(), - WithHandler( - opencode_handler(), - multi_perspective_analysis(topic), - ), - ) + program = preset_handlers()(opencode_handler()(multi_perspective_analysis(topic))) result = await async_run(program, handlers=default_handlers()) if result.is_err(): diff --git a/packages/doeff-agentic/examples/07_pr_review_workflow.py b/packages/doeff-agentic/examples/07_pr_review_workflow.py index 14c073620..d50711201 100644 --- a/packages/doeff-agentic/examples/07_pr_review_workflow.py +++ b/packages/doeff-agentic/examples/07_pr_review_workflow.py @@ -209,7 +209,7 @@ def pr_review_workflow(pr_url: str, require_approval: bool = False): import asyncio import sys - from doeff import WithHandler, async_run, default_handlers + from doeff import async_run, default_handlers async def main(): # Use a sample PR URL or accept from command line @@ -226,13 +226,7 @@ async def main(): # Merge preset handlers with opencode handlers # Preset provides: slog display (WriterTellEffect) + config (Ask preset.*) # OpenCode provides: agent session management effects - program = WithHandler( - preset_handlers(), - WithHandler( - opencode_handler(), - pr_review_workflow(pr_url, require_approval), - ), - ) + program = preset_handlers()(opencode_handler()(pr_review_workflow(pr_url, require_approval))) try: result = await async_run(program, handlers=default_handlers()) diff --git a/packages/doeff-agentic/examples/08_testing_with_mocks.py b/packages/doeff-agentic/examples/08_testing_with_mocks.py index 5b6ea80c3..09ef64c42 100644 --- a/packages/doeff-agentic/examples/08_testing_with_mocks.py +++ b/packages/doeff-agentic/examples/08_testing_with_mocks.py @@ -22,7 +22,7 @@ ) from doeff_agentic.handlers.testing import MockAgenticHandler, mock_handlers -from doeff import WithHandler, default_handlers, do, run +from doeff import default_handlers, do, run @do @@ -42,7 +42,7 @@ def mock_conversation(): def main() -> int: # Compose the mock protocol handler directly with WithHandler. handler_impl = MockAgenticHandler(workflow_name="mock-example") - program = WithHandler(mock_handlers(handler_impl), mock_conversation()) + program = mock_handlers(handler_impl)(mock_conversation()) result = run(program, handlers=default_handlers()) if result.is_err(): diff --git a/packages/doeff-agentic/src/doeff_agentic/handlers/opencode.py b/packages/doeff-agentic/src/doeff_agentic/handlers/opencode.py index 823934307..6a1ab6a66 100644 --- a/packages/doeff-agentic/src/doeff_agentic/handlers/opencode.py +++ b/packages/doeff-agentic/src/doeff_agentic/handlers/opencode.py @@ -7,11 +7,10 @@ import asyncio from doeff import async_run, default_handlers from doeff_agentic.opencode_handler import opencode_handler - from doeff import WithHandler async def main(): handlers = opencode_handler() - program = WithHandler(handlers, my_workflow()) + program = handlers(my_workflow()) result = await async_run(program, handlers=default_handlers()) asyncio.run(main()) @@ -31,6 +30,7 @@ async def main(): from typing import Any from doeff import Await, Effect, Pass, Resume, do, slog +from doeff import handler as _program_handler # REMOVED: make_doeff_generator no longer exists in doeff.do # from doeff.do import make_doeff_generator @@ -1032,7 +1032,7 @@ def _wrapped(effect: Effect, k: Any): return (yield Resume(k, result)) - return _wrapped + return _program_handler(_wrapped) def opencode_handler( @@ -1052,17 +1052,16 @@ def opencode_handler( working_dir: Default working directory Returns: - Protocol handler for use with WithHandler composition. + Protocol handler for direct Program -> Program composition. Usage: import asyncio from doeff import async_run, default_handlers from doeff_agentic import opencode_handler - from doeff import WithHandler async def main(): handlers = opencode_handler() - program = WithHandler(handlers, my_workflow()) + program = handlers(my_workflow()) result = await async_run(program, handlers=default_handlers()) asyncio.run(main()) @@ -1100,7 +1099,7 @@ def protocol_handler(effect: Effect, k: Any): return (yield effect_handler(effect, k)) yield Pass() - return protocol_handler + return _program_handler(protocol_handler) __all__ = [ diff --git a/packages/doeff-agentic/src/doeff_agentic/handlers/production.py b/packages/doeff-agentic/src/doeff_agentic/handlers/production.py index fdbe4b7f1..2c0ee5a7a 100644 --- a/packages/doeff-agentic/src/doeff_agentic/handlers/production.py +++ b/packages/doeff-agentic/src/doeff_agentic/handlers/production.py @@ -40,7 +40,8 @@ StopEffect, ) -from doeff import Effect, Pass, Resume, WithHandler, do +from doeff import Effect, Pass, Resume, do +from doeff import handler as _program_handler from doeff_agentic.effects import ( AgentNotRunningError, CaptureOutputEffect, @@ -551,7 +552,7 @@ def agentic_effectful_handlers( # noqa: PLR0915 - baseline cleanup keeps existi The returned handler follows doeff_vm's public handler protocol: `(effect, k) -> DoExpr`. - Use with `doeff.WithHandler` to scope handling to a specific program. + Call the returned Program -> Program handler to scope it to a specific program. See `with_agentic_effectful_handlers()` for the convenience wrapper. """ handler = AgenticHandler( @@ -689,7 +690,7 @@ def _handle(effect: Effect, k: Any): # noqa: PLR0911, PLR0912, PLR0915 - baseli return (yield Resume(k, value)) yield Pass() - return _handle + return _program_handler(_handle) def with_agentic_effectful_handlers( @@ -700,15 +701,12 @@ def with_agentic_effectful_handlers( tmux_handler: TmuxAgentHandler | None = None, ) -> Any: """Wrap a program with the legacy agentic effect handler.""" - return WithHandler( - handler=agentic_effectful_handlers( + return agentic_effectful_handlers( workflow_id=workflow_id, workflow_name=workflow_name, state_dir=state_dir, tmux_handler=tmux_handler, - ), - expr=program, - ) + )(program) def agent_handler( diff --git a/packages/doeff-agentic/src/doeff_agentic/handlers/testing.py b/packages/doeff-agentic/src/doeff_agentic/handlers/testing.py index cece0064b..517d3a266 100644 --- a/packages/doeff-agentic/src/doeff_agentic/handlers/testing.py +++ b/packages/doeff-agentic/src/doeff_agentic/handlers/testing.py @@ -10,6 +10,7 @@ from typing import Any from doeff import Effect, Pass, Resume, do +from doeff import handler as _program_handler # REMOVED: make_doeff_generator no longer exists in doeff.do # from doeff.do import make_doeff_generator @@ -451,7 +452,7 @@ def _wrapped(effect: Effect, k: Any): return (yield Resume(k, result)) - return _wrapped + return _program_handler(_wrapped) def mock_handlers( @@ -492,7 +493,7 @@ def protocol_handler(effect: Effect, k: Any): return (yield effect_handler(effect, k)) yield Pass() - return protocol_handler + return _program_handler(protocol_handler) __all__ = [ diff --git a/packages/doeff-agentic/src/doeff_agentic/handlers/tmux.py b/packages/doeff-agentic/src/doeff_agentic/handlers/tmux.py index b51c8a6ea..a9ae86cc8 100644 --- a/packages/doeff-agentic/src/doeff_agentic/handlers/tmux.py +++ b/packages/doeff-agentic/src/doeff_agentic/handlers/tmux.py @@ -13,10 +13,9 @@ Usage: from doeff import default_handlers, run from doeff_agentic.tmux_handler import tmux_handler - from doeff import WithHandler handlers = tmux_handler() - program = WithHandler(handlers, my_workflow()) + program = handlers(my_workflow()) result = run(program, handlers=default_handlers()) """ @@ -35,6 +34,7 @@ from typing import Any from doeff import Effect, Pass, Resume, do +from doeff import handler as _program_handler # REMOVED: make_doeff_generator no longer exists in doeff.do # from doeff.do import make_doeff_generator @@ -938,7 +938,7 @@ def _wrapped(effect: Effect, k: Any): return (yield Resume(k, result)) - return _wrapped + return _program_handler(_wrapped) def tmux_handler( @@ -952,15 +952,14 @@ def tmux_handler( working_dir: Default working directory Returns: - Protocol handler for use with WithHandler composition. + Protocol handler for direct Program -> Program composition. Usage: from doeff import default_handlers, run from doeff_agentic import tmux_handler - from doeff import WithHandler handlers = tmux_handler() - program = WithHandler(handlers, my_workflow()) + program = handlers(my_workflow()) result = run(program, handlers=default_handlers()) """ handler = TmuxHandler(working_dir=working_dir, backend=backend) @@ -990,7 +989,7 @@ def protocol_handler(effect: Effect, k: Any): return (yield effect_handler(effect, k)) yield Pass() - return protocol_handler + return _program_handler(protocol_handler) __all__ = [ diff --git a/packages/doeff-agentic/src/doeff_agentic/runtime.py b/packages/doeff-agentic/src/doeff_agentic/runtime.py index c906123ea..2dc577340 100644 --- a/packages/doeff-agentic/src/doeff_agentic/runtime.py +++ b/packages/doeff-agentic/src/doeff_agentic/runtime.py @@ -4,14 +4,14 @@ from collections.abc import Sequence from typing import Any -from doeff import WithHandler +from doeff import handler as _program_handler def with_handlers(program: Any, handlers: Sequence[Any]) -> Any: """Wrap ``program`` with handlers ordered from outer to inner.""" wrapped = program for handler in reversed(tuple(handlers)): - wrapped = WithHandler(handler=handler, expr=wrapped) + wrapped = _program_handler(handler)(wrapped) return wrapped diff --git a/packages/doeff-agentic/tests/test_handlers_module.py b/packages/doeff-agentic/tests/test_handlers_module.py index 5c9384b52..a8b1d4876 100644 --- a/packages/doeff-agentic/tests/test_handlers_module.py +++ b/packages/doeff-agentic/tests/test_handlers_module.py @@ -15,7 +15,7 @@ from doeff_agentic.handlers.production import agentic_effectful_handlers from doeff_agentic.types import AgentConfig -from doeff import WithHandler, default_handlers, do, run +from doeff import default_handlers, do, run def test_handlers_init_exports_required_factory_functions() -> None: @@ -45,7 +45,7 @@ def workflow(): status = yield AgenticGetSessionStatus(session_id=session.id) return msg.role, messages[-1].role, status.value - program = WithHandler(mock_handlers(), workflow()) + program = mock_handlers()(workflow()) result = run(program, handlers=default_handlers()) assert result.value == ("user", "assistant", "done") @@ -126,14 +126,11 @@ def workflow(): ) result = run( - WithHandler( - agentic_effectful_handlers( + agentic_effectful_handlers( workflow_id="wf-test", workflow_name="wf-test", tmux_handler=fake, - ), - workflow(), - ), + )(workflow()), handlers=default_handlers(), ) diff --git a/packages/doeff-agentic/tests/test_visual_interceptor.py b/packages/doeff-agentic/tests/test_visual_interceptor.py index 2a5afbd3a..6e76328a7 100644 --- a/packages/doeff-agentic/tests/test_visual_interceptor.py +++ b/packages/doeff-agentic/tests/test_visual_interceptor.py @@ -11,7 +11,8 @@ ) from rich.console import Console -from doeff import Effect, WithHandler, default_handlers, do, run, slog +from doeff import Effect, default_handlers, do, run, slog +from doeff import handler as _install_raw_handler @do @@ -41,7 +42,7 @@ def _make_config(buffer: StringIO) -> VisualInterceptorConfig: def test_with_visual_logging_logs_and_preserves_result() -> None: buffer = StringIO() wrapped = with_visual_logging(_workflow(), _make_config(buffer)) - result = run(WithHandler(_capability_handler, wrapped), handlers=default_handlers()) + result = run(_install_raw_handler(_capability_handler)(wrapped), handlers=default_handlers()) assert result.is_ok() assert result.value == "supported=True" @@ -55,7 +56,7 @@ def test_with_visual_logging_logs_and_preserves_result() -> None: def test_visual_logging_console_wrapper_functions() -> None: buffer = StringIO() wrapper, _console = visual_logging_console(_make_config(buffer)) - result = run(WithHandler(_capability_handler, wrapper(_workflow())), handlers=default_handlers()) + result = run(_install_raw_handler(_capability_handler)(wrapper(_workflow())), handlers=default_handlers()) assert result.is_ok() assert result.value == "supported=True" diff --git a/packages/doeff-agents/examples/_runtime.py b/packages/doeff-agents/examples/_runtime.py index f550d8baa..889e457a5 100644 --- a/packages/doeff-agents/examples/_runtime.py +++ b/packages/doeff-agents/examples/_runtime.py @@ -4,7 +4,8 @@ from collections.abc import Callable, Sequence from typing import Any -from doeff import WithHandler, async_run, default_handlers +from doeff import async_run, default_handlers +from doeff import handler as _program_handler ProtocolHandler = Callable[[Any, Any], Any] @@ -20,7 +21,7 @@ async def run_program( """Run a program with explicit ``WithHandler`` stacking plus runtime handlers.""" wrapped = program for handler in reversed(tuple(scoped_handlers)): - wrapped = WithHandler(handler=handler, expr=wrapped) + wrapped = _program_handler(handler)(wrapped) return await async_run( wrapped, diff --git a/packages/doeff-agents/src/doeff_agents/handlers/__init__.py b/packages/doeff-agents/src/doeff_agents/handlers/__init__.py index 1c06e547e..c136b87e0 100644 --- a/packages/doeff-agents/src/doeff_agents/handlers/__init__.py +++ b/packages/doeff-agents/src/doeff_agents/handlers/__init__.py @@ -8,7 +8,8 @@ from doeff_time import sync_time_handler -from doeff import Effect, Resume, WithHandler, do, run +from doeff import Effect, Resume, do, run +from doeff import handler as _program_handler from doeff.mcp import McpToolDef from doeff_agents.agentd_client import LazyAgentdClient from doeff_agents.effects import ( @@ -124,7 +125,7 @@ def make_scheduled_handler(handler: SimpleHandler) -> ProtocolHandler: def scheduled_handler(effect: Effect, k: Any): return (yield Resume(k, handler(effect))) - return scheduled_handler + return _program_handler(scheduled_handler) def _make_run_tool(handlers: list) -> Callable[[McpToolDef, dict], Any]: @@ -132,7 +133,7 @@ def _make_run_tool(handlers: list) -> Callable[[McpToolDef, dict], Any]: Each MCP tool call: 1. Builds a DoExpr program from tool.handler(*args) - 2. Wraps it with the captured handler stack via WithHandler + 2. Wraps it with the captured handler stack by calling each handler 3. Runs it via doeff.run() """ @@ -140,7 +141,7 @@ def run_tool(tool: McpToolDef, arguments: dict) -> Any: args = [arguments.get(name) for name in tool.param_names()] program = tool.handler(*args) for h in handlers: - program = WithHandler(h, program) + program = _program_handler(h)(program) return run(program) return run_tool @@ -174,7 +175,7 @@ def claude_agent_handler(*, backend=None): Usage: handler = claude_agent_handler() - wrapped = WithHandler(handler, program) + wrapped = handler(program) run(wrapped) """ import hy # noqa: F401 # activate Hy import hook diff --git a/packages/doeff-agents/src/doeff_agents/handlers/effectful.hy b/packages/doeff-agents/src/doeff_agents/handlers/effectful.hy index 935eafca9..749b5daba 100644 --- a/packages/doeff-agents/src/doeff_agents/handlers/effectful.hy +++ b/packages/doeff-agents/src/doeff_agents/handlers/effectful.hy @@ -6,7 +6,7 @@ (require doeff-hy.handle [defhandler]) (require doeff-hy.macros [<-]) -(import doeff [Ask GetHandlers WithHandler run]) +(import doeff [Ask GetHandlers run]) (import doeff_agents.effects [ AgentEffect AttachAgentSessionEffect @@ -35,7 +35,7 @@ (setv args (lfor name (.param-names tool) (.get arguments name))) (setv program (tool.handler #* args)) (for [handler handlers] - (setv program (WithHandler handler program))) + (setv program (handler program))) (run program)) run-tool) diff --git a/packages/doeff-agents/src/doeff_agents/handlers/mcp_server_loop.hy b/packages/doeff-agents/src/doeff_agents/handlers/mcp_server_loop.hy index 39d4b91db..2e405a0b5 100644 --- a/packages/doeff-agents/src/doeff_agents/handlers/mcp_server_loop.hy +++ b/packages/doeff-agents/src/doeff_agents/handlers/mcp_server_loop.hy @@ -20,7 +20,6 @@ ;;; WaitUntil, GetTime, and other time-sensitive effects actually work. (require doeff-hy.macros [<- defk]) -(import doeff [WithHandler]) (import doeff_core_effects.scheduler [CreateExternalPromise Wait Spawn PRIORITY_IDLE]) (import doeff_agents.mcp-server [McpToolServer McpToolRequest]) (import queue [Empty]) @@ -42,7 +41,7 @@ (setv args (lfor name (.param-names tool) (.get req.arguments name))) (setv program (tool.handler #* args)) (for [h full-stack] - (setv program (WithHandler h program))) + (setv program (h program))) (setv ok True) (setv result None) (setv error-msg None) diff --git a/packages/doeff-agents/src/doeff_agents/handlers/testing.py b/packages/doeff-agents/src/doeff_agents/handlers/testing.py index 11282bdf7..1e3ce3cc1 100644 --- a/packages/doeff-agents/src/doeff_agents/handlers/testing.py +++ b/packages/doeff-agents/src/doeff_agents/handlers/testing.py @@ -4,7 +4,8 @@ from dataclasses import dataclass, field from datetime import datetime, timezone -from doeff import Effect, Pass, Resume, WithHandler, do +from doeff import Effect, Pass, Resume, do +from doeff import handler as _install_raw_handler from doeff_agents.adapters.base import AgentType from doeff_agents.effects import ( AgentEffect, @@ -439,7 +440,7 @@ def handler(effect: Effect, k): return (yield Resume(k, None)) yield Pass(effect, k) - return WithHandler(handler, program) + return _install_raw_handler(handler)(program) def handle_launch_session(self, effect: LaunchSessionEffect) -> L2SessionHandle: session_id = effect.spec.session_id diff --git a/packages/doeff-agents/tests/test_agent_session_state.py b/packages/doeff-agents/tests/test_agent_session_state.py index 497185074..fdc31bd47 100644 --- a/packages/doeff-agents/tests/test_agent_session_state.py +++ b/packages/doeff-agents/tests/test_agent_session_state.py @@ -6,7 +6,8 @@ import time from pathlib import Path -from doeff import WithHandler, do, run +from doeff import do, run +from doeff import handler as _program_handler sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) @@ -29,7 +30,7 @@ def _install_handlers(handlers, program): wrapped = program for handler in reversed(handlers): - wrapped = WithHandler(handler, wrapped) + wrapped = _program_handler(handler)(wrapped) return wrapped diff --git a/packages/doeff-agents/tests/test_claude_handler.py b/packages/doeff-agents/tests/test_claude_handler.py index f64833525..72ef56cac 100644 --- a/packages/doeff-agents/tests/test_claude_handler.py +++ b/packages/doeff-agents/tests/test_claude_handler.py @@ -11,7 +11,7 @@ ) from doeff_core_effects.handlers import state -from doeff import Perform, WithHandler, do, run +from doeff import Perform, do, run class FakeTmuxBackend: @@ -49,7 +49,7 @@ def _run(program, backend): from doeff_agents.handlers.claude import claude_handler from doeff_core_effects.scheduler import scheduled handler = claude_handler(backend=backend) - wrapped = WithHandler(state(), WithHandler(handler, program)) + wrapped = state()(handler(program)) return run(scheduled(wrapped)) diff --git a/packages/doeff-agents/tests/test_claude_integration.py b/packages/doeff-agents/tests/test_claude_integration.py index 60387bfe7..89cdca0c6 100644 --- a/packages/doeff-agents/tests/test_claude_integration.py +++ b/packages/doeff-agents/tests/test_claude_integration.py @@ -19,7 +19,8 @@ from doeff_agents.session_backend import SessionBackend from doeff_core_effects.handlers import lazy_ask, state -from doeff import EffectBase, Pass, Perform, Resume, WithHandler, do, run +from doeff import EffectBase, Pass, Perform, Resume, do, run +from doeff import handler as _install_raw_handler # Fake tmux backend (same as test_claude_handler.py) @@ -77,13 +78,7 @@ def _run_full_stack(program, backend): from doeff_core_effects.scheduler import scheduled ch = claude_agent_handler(backend=backend) - wrapped = WithHandler( - state(), - WithHandler( - ch, - WithHandler(greet_handler, program), - ), - ) + wrapped = state()(ch(_install_raw_handler(greet_handler)(program))) return run(scheduled(wrapped)) @@ -130,13 +125,7 @@ def program(): )) return handle - wrapped = WithHandler( - lazy_ask(env={SessionBackend: backend}), - WithHandler( - state(), - WithHandler(claude_agent_handler(), program()), - ), - ) + wrapped = lazy_ask(env={SessionBackend: backend})(state()(claude_agent_handler()(program()))) handle = run(scheduled(wrapped)) diff --git a/packages/doeff-agents/tests/test_codex_handler.py b/packages/doeff-agents/tests/test_codex_handler.py index 44960b7bf..4f2cd16ba 100644 --- a/packages/doeff-agents/tests/test_codex_handler.py +++ b/packages/doeff-agents/tests/test_codex_handler.py @@ -7,7 +7,7 @@ from doeff_agents.session_backend import SessionBackend from doeff_core_effects.handlers import lazy_ask, state -from doeff import Perform, WithHandler, do, run +from doeff import Perform, do, run class FakeTmuxBackend: @@ -50,7 +50,7 @@ def _run(program, backend): from doeff_core_effects.scheduler import scheduled handler = codex_handler(backend=backend) - wrapped = WithHandler(state(), WithHandler(handler, program)) + wrapped = state()(handler(program)) return run(scheduled(wrapped)) @@ -89,13 +89,7 @@ def program(): prompt="hello", ))) - wrapped = WithHandler( - lazy_ask(env={SessionBackend: backend}), - WithHandler( - state(), - WithHandler(codex_handler(), program()), - ), - ) + wrapped = lazy_ask(env={SessionBackend: backend})(state()(codex_handler()(program()))) handle = run(scheduled(wrapped)) diff --git a/packages/doeff-agents/tests/test_handler_protocol.py b/packages/doeff-agents/tests/test_handler_protocol.py index b2b0b7d56..fba3e92ef 100644 --- a/packages/doeff-agents/tests/test_handler_protocol.py +++ b/packages/doeff-agents/tests/test_handler_protocol.py @@ -7,7 +7,8 @@ from dataclasses import dataclass from pathlib import Path -from doeff import Effect, EffectBase, Pass, Resume, WithHandler, do, run +from doeff import Effect, EffectBase, Pass, Resume, do, run +from doeff import handler as _install_raw_handler sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) @@ -89,10 +90,7 @@ def test_agent_handlers_are_defhandler_program_wrappers() -> None: def test_unknown_effect_delegates() -> None: result = run( - WithHandler( - _unknown_effect_fallback, - agent_effectful_handler()(_unknown_workflow()), - ) + _install_raw_handler(_unknown_effect_fallback)(agent_effectful_handler()(_unknown_workflow())) ) assert result == "noop" @@ -124,9 +122,9 @@ def _install_handlers(handlers, program): wrapped = program for handler in reversed(handlers): if tuple(inspect.signature(handler).parameters) == ("__doeff_body__",): - wrapped = handler(wrapped) + wrapped = _install_raw_handler(handler)(wrapped) else: - wrapped = WithHandler(handler, wrapped) + wrapped = _install_raw_handler(handler)(wrapped) return wrapped diff --git a/packages/doeff-agents/tests/test_mcp_e2e.py b/packages/doeff-agents/tests/test_mcp_e2e.py index 7f96dd14a..415fe5beb 100644 --- a/packages/doeff-agents/tests/test_mcp_e2e.py +++ b/packages/doeff-agents/tests/test_mcp_e2e.py @@ -22,10 +22,10 @@ EffectBase, Perform, Resume, - WithHandler, do, run, ) +from doeff import handler as _install_raw_handler from doeff.mcp import McpParamSchema, McpToolDef # --------------------------------------------------------------------------- @@ -107,13 +107,7 @@ def _run_with_handlers(self, program): mock_handler = MockAgentHandler() agent_protocol = _make_protocol_handler(mock_handler) - wrapped = WithHandler( - agent_protocol, - WithHandler( - greet_handler, - WithHandler(upper_handler, program), - ), - ) + wrapped = agent_protocol(_install_raw_handler(greet_handler)(_install_raw_handler(upper_handler)(program))) return run(wrapped), mock_handler def test_launch_with_mcp_creates_server_and_mcp_json(self, tmp_path): diff --git a/packages/doeff-agents/tests/test_mcp_live_e2e.py b/packages/doeff-agents/tests/test_mcp_live_e2e.py index bb2b0baa4..91bee1b61 100644 --- a/packages/doeff-agents/tests/test_mcp_live_e2e.py +++ b/packages/doeff-agents/tests/test_mcp_live_e2e.py @@ -26,7 +26,7 @@ from doeff_agents.tmux import TmuxSessionBackend from doeff_time import Delay, sync_time_handler -from doeff import Perform, WithHandler, do, run +from doeff import Perform, do, run from doeff.mcp import McpParamSchema, McpToolDef logging.basicConfig(level=logging.INFO) @@ -100,7 +100,7 @@ def program(): return output try: - output = run(WithHandler(sync_time_handler(), WithHandler(agent_protocol, program()))) + output = run(sync_time_handler()(agent_protocol(program()))) # Verify the tool was actually called via the in-process log assert len(_tool_call_log) > 0, ( diff --git a/packages/doeff-agents/tests/test_mixed_agent_handlers.py b/packages/doeff-agents/tests/test_mixed_agent_handlers.py index e2a0e5293..8572dc44d 100644 --- a/packages/doeff-agents/tests/test_mixed_agent_handlers.py +++ b/packages/doeff-agents/tests/test_mixed_agent_handlers.py @@ -6,7 +6,7 @@ from doeff_agents.effects.agent import LaunchEffect, StopEffect from doeff_core_effects.handlers import state -from doeff import Perform, WithHandler, do, run +from doeff import Perform, do, run class FakeTmuxBackend: @@ -63,13 +63,7 @@ def program(): yield Perform(StopEffect(handle=handle)) return "stopped" - wrapped = WithHandler( - state(), - WithHandler( - codex_handler(backend=codex_backend), - WithHandler(claude_handler(backend=claude_backend), program()), - ), - ) + wrapped = state()(codex_handler(backend=codex_backend)(claude_handler(backend=claude_backend)(program()))) result = run(scheduled(wrapped)) diff --git a/packages/doeff-agents/tests/test_session_backend.py b/packages/doeff-agents/tests/test_session_backend.py index d294af978..4ff7109bb 100644 --- a/packages/doeff-agents/tests/test_session_backend.py +++ b/packages/doeff-agents/tests/test_session_backend.py @@ -10,7 +10,7 @@ from doeff_core_effects.handlers import lazy_ask -from doeff import WithHandler, do, run +from doeff import do, run sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) @@ -338,10 +338,7 @@ def workflow(): return observation.status result = run( - WithHandler( - lazy_ask(env={SessionBackend: backend}), - agent_effectful_handler()(workflow()), - ) + lazy_ask(env={SessionBackend: backend})(agent_effectful_handler()(workflow())) ) assert result == SessionStatus.EXITED diff --git a/packages/doeff-agents/tests/test_withhandler_agent_delegation.py b/packages/doeff-agents/tests/test_withhandler_agent_delegation.py index dfe15967b..cab71bf36 100644 --- a/packages/doeff-agents/tests/test_withhandler_agent_delegation.py +++ b/packages/doeff-agents/tests/test_withhandler_agent_delegation.py @@ -8,7 +8,8 @@ from doeff_time import DelayEffect -from doeff import Effect, Pass, Resume, WithHandler, do, run +from doeff import Effect, Pass, Resume, do, run +from doeff import handler as _install_raw_handler sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) @@ -166,7 +167,7 @@ def test_withhandler_delegation_returns_success() -> None: ) result = run( - WithHandler(handler, _run_completion("worker", _build_config(), poll_interval=0.5)), + _install_raw_handler(handler)(_run_completion("worker", _build_config(), poll_interval=0.5)), ) assert result.final_status == SessionStatus.DONE @@ -189,7 +190,7 @@ def test_monitor_agent_to_completion_cleans_existing_session() -> None: handle = _session_handle("existing", AgentType.CODEX) result = run( - WithHandler(handler, _monitor_existing_completion(handle, poll_interval=0.5)), + _install_raw_handler(handler)(_monitor_existing_completion(handle, poll_interval=0.5)), ) assert result.final_status == SessionStatus.DONE @@ -209,7 +210,7 @@ def test_withhandler_delegation_returns_failure_status() -> None: ) result = run( - WithHandler(handler, _run_completion("worker-fail", _build_config())), + _install_raw_handler(handler)(_run_completion("worker-fail", _build_config())), ) assert result.final_status == SessionStatus.FAILED @@ -228,7 +229,7 @@ def test_withhandler_multiple_agent_delegations_in_sequence() -> None: ) result = run( - WithHandler(handler, _run_two_completions(_build_config())), + _install_raw_handler(handler)(_run_two_completions(_build_config())), ) first, second = result @@ -263,13 +264,7 @@ def launch_only_handler(effect: Effect, k): {"typed-flow": [(SessionStatus.DONE, "typed done")]} ) - wrapped = WithHandler( - lifecycle_handler, - WithHandler( - launch_only_handler, - _run_completion("typed-flow", _build_config(), poll_interval=0.0), - ), - ) + wrapped = lifecycle_handler(_install_raw_handler(launch_only_handler)(_run_completion("typed-flow", _build_config(), poll_interval=0.0))) result = run(wrapped) @@ -298,17 +293,11 @@ def primary_handler(effect: Effect, k): launch_agent_override=AgentType.CODEX, ) - wrapped = WithHandler( - fallback_handler, - WithHandler( - primary_handler, - _run_completion( + wrapped = fallback_handler(_install_raw_handler(primary_handler)(_run_completion( "fallback-agent", _build_config(agent_type=AgentType.CLAUDE), poll_interval=0.0, - ), - ), - ) + ))) result = run(wrapped) @@ -330,14 +319,11 @@ def test_interactive_session_launches_with_interactive_lifecycle() -> None: } ) - wrapped = WithHandler( - handler, - _run_interactive( + wrapped = _install_raw_handler(handler)(_run_interactive( "chat", _build_config(agent_type=AgentType.CODEX), ["continue"], - ), - ) + )) result = run(wrapped) diff --git a/packages/doeff-conductor/src/doeff_conductor/api.py b/packages/doeff-conductor/src/doeff_conductor/api.py index a9425c1f0..15f482bf5 100644 --- a/packages/doeff-conductor/src/doeff_conductor/api.py +++ b/packages/doeff-conductor/src/doeff_conductor/api.py @@ -133,7 +133,7 @@ def run_workflow( # noqa: PLR0912, PLR0915 # Execute the workflow from doeff_core_effects.scheduler import scheduled - from doeff import WithHandler, run + from doeff import run # Build kwargs kwargs = params or {} @@ -166,7 +166,7 @@ def run_workflow( # noqa: PLR0912, PLR0915 journal_run_id=workflow_id, ) - result = run(scheduled(WithHandler(conductor_handler, program))) + result = run(scheduled(conductor_handler(program))) result_value = result.value if type(result).__name__ == "RunResult" else result open_gates = () from doeff_conductor.workflow_runtime import ParkedValue, WorkflowRuntimeResult diff --git a/packages/doeff-conductor/src/doeff_conductor/handlers/__init__.py b/packages/doeff-conductor/src/doeff_conductor/handlers/__init__.py index 27b015b01..b6f94ae3c 100644 --- a/packages/doeff-conductor/src/doeff_conductor/handlers/__init__.py +++ b/packages/doeff-conductor/src/doeff_conductor/handlers/__init__.py @@ -6,7 +6,8 @@ from types import SimpleNamespace from typing import TYPE_CHECKING, Any -from doeff import WithHandler, run +from doeff import handler as _program_handler +from doeff import run from doeff_conductor.workflow_effect_journal import JournaledWorkflowEffectHandler from .agent_handler import ( @@ -136,7 +137,7 @@ def run_sync( wrapped_program = program for handler in reversed(protocol_handlers): - wrapped_program = WithHandler(handler, wrapped_program) + wrapped_program = _program_handler(handler)(wrapped_program) try: return RunSyncResult(value=run(wrapped_program)) except Exception as error: @@ -151,8 +152,8 @@ def run_sync( "GitHandler", "IssueHandler", "JournaledAgentHandler", - "JournaledWorkspaceHandler", "JournaledWorkflowEffectHandler", + "JournaledWorkspaceHandler", "MockConductorRuntime", "RunSyncResult", "WorkspaceHandler", diff --git a/packages/doeff-conductor/src/doeff_conductor/handlers/testing.py b/packages/doeff-conductor/src/doeff_conductor/handlers/testing.py index 6296d534f..a5a026720 100644 --- a/packages/doeff-conductor/src/doeff_conductor/handlers/testing.py +++ b/packages/doeff-conductor/src/doeff_conductor/handlers/testing.py @@ -12,7 +12,8 @@ from doeff_agents.result_validation import validate_result_payload -from doeff import Effect, Gather, Pass, Resume, Spawn, WithHandler, do +from doeff import Effect, Gather, Pass, Resume, Spawn, do +from doeff import handler as _install_raw_handler from doeff_conductor.effects.agent import ( AgentAttemptExhaustedError, AgentEffect, @@ -412,7 +413,7 @@ def handler(effect: Effect, k: Any): if isinstance(effect, Spawn): spawn_counter += 1 task_id = f"mock-spawn-{spawn_counter}" - result = yield WithHandler(handler, effect.program) + result = yield _install_raw_handler(handler)(effect.program) spawn_results[task_id] = result return (yield Resume(k, task_id)) @@ -425,7 +426,7 @@ def handler(effect: Effect, k: Any): return (yield effect_handler(effect, k)) yield Pass(effect, k) - return handler + return _install_raw_handler(handler) __all__ = [ diff --git a/packages/doeff-conductor/src/doeff_conductor/handlers/utils.py b/packages/doeff-conductor/src/doeff_conductor/handlers/utils.py index 0b93e7818..4bdfd6c51 100644 --- a/packages/doeff-conductor/src/doeff_conductor/handlers/utils.py +++ b/packages/doeff-conductor/src/doeff_conductor/handlers/utils.py @@ -7,6 +7,7 @@ from doeff_core_effects.scheduler import CreateExternalPromise, Wait from doeff import Effect, Pass, Resume, do +from doeff import handler as _install_raw_handler if TYPE_CHECKING: from doeff_conductor.handlers.agent_handler import AgentHandler @@ -166,7 +167,7 @@ def handler(effect: Effect, k: Any): return (yield effect_handler(effect, k)) yield Pass(effect, k) - return handler + return _install_raw_handler(handler) __all__ = [ diff --git a/packages/doeff-conductor/tests/test_k4_parallel_overlap.py b/packages/doeff-conductor/tests/test_k4_parallel_overlap.py index c9aeb06a9..1d8cc1ba2 100644 --- a/packages/doeff-conductor/tests/test_k4_parallel_overlap.py +++ b/packages/doeff-conductor/tests/test_k4_parallel_overlap.py @@ -27,7 +27,8 @@ ) from doeff_core_effects.scheduler import Gather, scheduled -from doeff import Effect, Pass, Resume, Spawn, WithHandler, do, run +from doeff import Effect, Pass, Resume, Spawn, do, run +from doeff import handler as _install_raw_handler SIMPLE_SCHEMA: dict[str, Any] = { "type": "object", @@ -139,7 +140,7 @@ def handler(effect: Effect, k: Any): return (yield effect_handler(effect, k)) yield Pass(effect, k) - return handler + return _install_raw_handler(handler) class TestK4ParallelOverlap: @@ -181,7 +182,7 @@ def parallel_workflow(): return results wall_start = time.monotonic() - result = run(scheduled(WithHandler(conductor_handler, parallel_workflow()))) + result = run(scheduled(conductor_handler(parallel_workflow()))) wall_elapsed = time.monotonic() - wall_start # Unwrap RunResult if needed @@ -240,7 +241,7 @@ def workflow(iteration: int = iteration): return results wall_start = time.monotonic() - result = run(scheduled(WithHandler(conductor_handler, workflow()))) + result = run(scheduled(conductor_handler(workflow()))) wall_elapsed = time.monotonic() - wall_start result_value = result.value if hasattr(result, "value") else result @@ -293,7 +294,7 @@ def workflow(): return results wall_start = time.monotonic() - run(scheduled(WithHandler(blocking_handler, workflow()))) + run(scheduled(_install_raw_handler(blocking_handler)(workflow()))) wall_elapsed = time.monotonic() - wall_start # The blocking handler should take >= sum of branch delays diff --git a/packages/doeff-core-effects/doeff_core_effects/cache_handlers.py b/packages/doeff-core-effects/doeff_core_effects/cache_handlers.py index 5489d3c38..53b7202af 100644 --- a/packages/doeff-core-effects/doeff_core_effects/cache_handlers.py +++ b/packages/doeff-core-effects/doeff_core_effects/cache_handlers.py @@ -8,6 +8,7 @@ from typing import TypeAlias from doeff import do +from doeff import handler as _program_handler from doeff.program import Pass, Resume from doeff_core_effects.cache_effects import ( CacheExists, @@ -139,7 +140,7 @@ def handler(effect, k): result = yield Resume(k, None) return result - return handler + return _program_handler(handler) def in_memory_cache_handler(): @@ -199,7 +200,7 @@ def handler(effect, k): result = yield Resume(k, delegated) return result - return handler + return _program_handler(handler) def memo_rewriters( diff --git a/packages/doeff-core-effects/doeff_core_effects/handlers.py b/packages/doeff-core-effects/doeff_core_effects/handlers.py index eb617fab9..2ea81aefa 100644 --- a/packages/doeff-core-effects/doeff_core_effects/handlers.py +++ b/packages/doeff-core-effects/doeff_core_effects/handlers.py @@ -10,7 +10,8 @@ """ from doeff import do -from doeff.program import Pass, Resume, WithHandlerType +from doeff.program import Pass, Resume +from doeff.program import handler as _program_handler from doeff_core_effects.effects import ( Ask, Await, @@ -24,20 +25,6 @@ ) -def _program_handler(raw_handler): - """Wrap a raw dispatcher as the current Program -> Program handler shape.""" - - def install(body): - return WithHandlerType(raw_handler, body) - - install.__name__ = raw_handler.__name__ - install.__qualname__ = raw_handler.__qualname__ - install.__doc__ = raw_handler.__doc__ - install._doeff_is_handler_fn = True - install.__doeff_handler_data__ = raw_handler - return install - - def reader(env=None): """Reader handler: resolves Ask(key) from env dict. @@ -104,7 +91,7 @@ def handler(effect, k): @do -def try_handler(effect, k): +def _try_handler(effect, k): """Try handler: catches errors from Try(program) and returns Ok/Err. Captures inner handlers (between body and try_handler) via GetHandlers @@ -112,16 +99,13 @@ def try_handler(effect, k): can reach handlers at any position in the chain. Usage: - WithHandler(try_handler, body) + try_handler(body) result = yield Try(some_program) # Ok(value) or Err(error) """ if isinstance(effect, Try): from doeff_vm import Err, Ok from doeff.handler_utils import get_inner_handlers - from doeff.program import ( - WithHandler as WH, # noqa: N817 - existing local alias keeps handler code compact - ) inner_hs = yield get_inner_handlers(k) @@ -131,8 +115,8 @@ def attempt(): # Reinstall inner handlers + try_handler itself so nested # Try effects and inner-handler effects are reachable. for h in inner_hs: - prog = WH(h, prog) - prog = WH(try_handler, prog) + prog = _program_handler(h)(prog) + prog = try_handler(prog) try: value = yield prog return Ok(value) @@ -143,6 +127,11 @@ def attempt(): yield Pass(effect, k) +try_handler = _program_handler(_try_handler) +try_handler.__name__ = "try_handler" +try_handler.__qualname__ = "try_handler" + + def slog_handler(): """Structured log handler: collects Slog messages. @@ -166,7 +155,7 @@ def handler(effect, k): @do -def local_handler(effect, k): +def _local_handler(effect, k): """Local handler: scoped env override with pass-on-miss semantics. Installs a scope reader that handles Ask for overridden keys only, @@ -178,13 +167,10 @@ def local_handler(effect, k): and reinstall them around the inner program. Usage: - WithHandler(local_handler, body) + local_handler(body) """ if isinstance(effect, Local): from doeff.handler_utils import get_inner_handlers - from doeff.program import ( - WithHandler as WH, # noqa: N817 - existing local alias keeps handler code compact - ) overrides = effect.env # Capture inner handlers from continuation (between Local site @@ -201,30 +187,32 @@ def scope_reader(inner_effect, inner_k): # then scope_reader innermost prog = effect.program for h in inner_handlers: - prog = WH(h, prog) - prog = WH(local_handler, prog) - prog = WH(scope_reader, prog) + prog = _program_handler(h)(prog) + prog = local_handler(prog) + prog = _program_handler(scope_reader)(prog) inner_result = yield prog return (yield Resume(k, inner_result)) yield Pass(effect, k) +local_handler = _program_handler(_local_handler) +local_handler.__name__ = "local_handler" +local_handler.__qualname__ = "local_handler" + + @do -def listen_handler(effect, k): +def _listen_handler(effect, k): """Listen handler: collects effects of specified types during program execution. OCaml 5 semantics: reinstall inner handlers so the inner program sees the same handler chain as when Listen was performed. Usage: - WithHandler(listen_handler, body) + listen_handler(body) """ if isinstance(effect, Listen): from doeff.handler_utils import get_inner_handlers - from doeff.program import ( - WithHandler as WH, # noqa: N817 - existing local alias keeps handler code compact - ) collected = [] types_to_collect = effect.types or (WriterTellEffect,) @@ -238,8 +226,8 @@ def observer_handler(inner_effect, inner_k): prog = effect.program for h in inner_handlers: - prog = WH(h, prog) - prog = WH(observer_handler, prog) + prog = _program_handler(h)(prog) + prog = _program_handler(observer_handler)(prog) inner_result = yield prog result = yield Resume(k, (inner_result, collected)) @@ -247,6 +235,11 @@ def observer_handler(inner_effect, inner_k): yield Pass(effect, k) +listen_handler = _program_handler(_listen_handler) +listen_handler.__name__ = "listen_handler" +listen_handler.__qualname__ = "listen_handler" + + def await_handler(): """Await handler: runs async coroutines via a background thread with asyncio. @@ -345,9 +338,6 @@ def lazy_ask(env=None, *, strict=False): # noqa: PLR0915 - baseline cleanup kee from doeff import Program from doeff.handler_utils import get_inner_handlers from doeff.program import ResumeThrow - from doeff.program import ( - WithHandler as WH, # noqa: N817 - existing local alias keeps handler code compact - ) from doeff_core_effects.scheduler import ( AcquireSemaphore, CreateSemaphore, @@ -436,12 +426,12 @@ def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup ke inner_hs = yield get_inner_handlers(k) wrapped = raw for h in inner_hs: - wrapped = WH(h, wrapped) + wrapped = _program_handler(h)(wrapped) eval_stack.append(set()) error = None value = None try: - value = yield WH(handler, wrapped) + value = yield _program_handler(handler)(wrapped) except Exception as e: error = e @@ -468,12 +458,12 @@ def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup ke inner_hs = yield get_inner_handlers(k) prog = effect.program for h in inner_hs: - prog = WH(h, prog) + prog = _program_handler(h)(prog) error = None inner_result = None try: - inner_result = yield WH(inner_handler, prog) + inner_result = yield inner_handler(prog) except Exception as e: error = e @@ -483,9 +473,9 @@ def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup ke yield Pass(effect, k) - return handler + return _program_handler(handler) - return _program_handler(_make_handler(dict(env))) + return _make_handler(dict(env)) def env_var_ask(*, prefix="DOEFF_"): @@ -517,9 +507,6 @@ def env_var_ask(*, prefix="DOEFF_"): from doeff import Program from doeff.cli.run_services import import_symbol from doeff.handler_utils import get_inner_handlers - from doeff.program import ( - WithHandler as WH, # noqa: N817 - existing local alias keeps handler code compact - ) from doeff_core_effects.scheduler import ( AcquireSemaphore, CreateSemaphore, @@ -582,7 +569,7 @@ def handler(effect, k): # noqa: PLR0911 - baseline cleanup keeps existing contr inner_hs = yield get_inner_handlers(k) wrapped = value for h in inner_hs: - wrapped = WH(h, wrapped) + wrapped = _program_handler(h)(wrapped) resolved = yield wrapped else: resolved = value diff --git a/packages/doeff-core-effects/doeff_core_effects/memo_handlers.py b/packages/doeff-core-effects/doeff_core_effects/memo_handlers.py index 7ac20bd51..96db95861 100644 --- a/packages/doeff-core-effects/doeff_core_effects/memo_handlers.py +++ b/packages/doeff-core-effects/doeff_core_effects/memo_handlers.py @@ -16,6 +16,7 @@ from typing import Any, TypeAlias from doeff import UnhandledEffect, do +from doeff import handler as _program_handler from doeff.program import Pass, Resume from doeff_core_effects.memo_effects import ( MemoExists, @@ -147,10 +148,9 @@ def memo_handler( Stack multiple handlers for layered caching: - WithHandler(memo_handler(minio, cost=EXPENSIVE, name="minio"), - WithHandler(memo_handler(redis, cost=CHEAP, name="redis"), - WithHandler(memo_handler(memory, name="L1"), - program))) + memo_handler(minio, cost=EXPENSIVE, name="minio")( + memo_handler(redis, cost=CHEAP, name="redis")( + memo_handler(memory, name="L1")(program))) Each handler is a caching proxy. Position determines terminal behavior: the outermost handler's miss re-perform is unhandled → memo_rewriter treats as miss. @@ -227,7 +227,7 @@ def handler(effect, k): # noqa: PLR0911 - baseline cleanup keeps existing contr result = yield Resume(k, None) return result - return handler + return _program_handler(handler) def in_memory_memo_handler(): @@ -312,7 +312,7 @@ def handler(effect, k): result = yield Resume(k, delegated) return result - return handler + return _program_handler(handler) def memo_rewriters( diff --git a/packages/doeff-core-effects/doeff_core_effects/scheduler.py b/packages/doeff-core-effects/doeff_core_effects/scheduler.py index 857f12336..532022c42 100644 --- a/packages/doeff-core-effects/doeff_core_effects/scheduler.py +++ b/packages/doeff-core-effects/doeff_core_effects/scheduler.py @@ -1,7 +1,7 @@ """ Cooperative scheduler — OCaml 5 recursive match_with pattern. -Each task gets its own WithHandler. Spawn creates a new WithHandler +Each task gets its own handler node. Spawn creates a new handler node recursively. All handler instances share state via closure. No envelope needed. Task completion is caught by a @do wrapper that @@ -25,7 +25,8 @@ def main(): from doeff.do import do from doeff.handler_utils import get_inner_handlers -from doeff.program import Pass, Perform, Pure, Resume, Transfer, WithHandler +from doeff.program import Pass, Perform, Pure, Resume, Transfer +from doeff.program import handler as _program_handler def _enrich_exception_traceback(exc, task_meta=None, vm_ctx=None): @@ -390,8 +391,8 @@ def pick_next(): # noqa: PLR0912 - scheduler dispatch loop has one branch per r prog = tasks[tid].pop("program") # Re-wrap task with inner handlers captured at spawn site for h in tasks[tid].pop("inner_handlers", []): - prog = WithHandler(h, prog) - return WithHandler(make_handler(tid), wrap_task(tid, prog)) + prog = _program_handler(h)(prog) + return make_handler(tid)(wrap_task(tid, prog)) if entry[0] == "resume": _, owner_tid, cont, value = entry if is_owner_cancelled(owner_tid): @@ -564,9 +565,9 @@ def pop_live_semaphore_waiter(sem): def make_handler(current_tid): @do - def handler(effect, k): + def raw_handler(effect, k): return (yield TailEval(handle_scheduler_effect(current_tid, effect, k))) - return handler + return _program_handler(raw_handler) @do def handle_scheduler_effect(current_tid, effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged @@ -793,4 +794,4 @@ def handle_scheduler_effect(current_tid, effect, k): # noqa: PLR0911, PLR0912, else: yield Pass(effect, k) - return WithHandler(make_handler(None), body_program) + return make_handler(None)(body_program) diff --git a/packages/doeff-docker/src/doeff_docker/compose.hy b/packages/doeff-docker/src/doeff_docker/compose.hy index c23598ba0..5f47a5012 100644 --- a/packages/doeff-docker/src/doeff_docker/compose.hy +++ b/packages/doeff-docker/src/doeff_docker/compose.hy @@ -7,13 +7,10 @@ (my-program)) Expands to: - (WithHandler (writer) - (WithHandler (slog-handler) - (WithHandler resolve-handler - (my-program)))) + ((writer) ((slog-handler) (resolve-handler (my-program)))) Handlers are applied inner-first: last in list is innermost." (setv result body) (for [h (reversed handlers)] - (setv result `(WithHandler ~h ~result))) + (setv result `(~h ~result))) result) diff --git a/packages/doeff-docker/tests/test_effects.py b/packages/doeff-docker/tests/test_effects.py index 5d0f3a8be..dac6df676 100644 --- a/packages/doeff-docker/tests/test_effects.py +++ b/packages/doeff-docker/tests/test_effects.py @@ -2,7 +2,7 @@ import hy # noqa: F401 import pytest -from doeff import run, WithHandler, Pure, do +from doeff import run, Pure, do from doeff_core_effects import reader, writer, slog_handler from doeff_docker.effects import ( @@ -16,10 +16,7 @@ def _run_with_handlers(program): return run( - WithHandler(writer(), - WithHandler(slog_handler(), - WithHandler(reader(env={}), - program))) + writer()(slog_handler()(reader(env={})(program))) ) diff --git a/packages/doeff-events/src/doeff_events/handlers/memory.py b/packages/doeff-events/src/doeff_events/handlers/memory.py index 2ab63a285..fe60354fd 100644 --- a/packages/doeff-events/src/doeff_events/handlers/memory.py +++ b/packages/doeff-events/src/doeff_events/handlers/memory.py @@ -4,6 +4,7 @@ from doeff_core_effects.scheduler import CompletePromise, CreatePromise, Wait from doeff import Pass, Resume, do +from doeff import handler as _program_handler from doeff_events.effects import PublishEffect, WaitForEventEffect @@ -60,7 +61,7 @@ def handler(effect, k): yield Pass(effect, k) - return handler + return _program_handler(handler) __all__ = ["event_handler"] diff --git a/packages/doeff-events/tests/test_multiple_listeners.py b/packages/doeff-events/tests/test_multiple_listeners.py index 09e976481..7ddedd84b 100644 --- a/packages/doeff-events/tests/test_multiple_listeners.py +++ b/packages/doeff-events/tests/test_multiple_listeners.py @@ -7,7 +7,7 @@ from doeff_events.effects import Publish, WaitForEvent from doeff_events.handlers import event_handler -from doeff import WithHandler, do, run +from doeff import do, run @dataclass(frozen=True) @@ -39,12 +39,12 @@ def run_listener(name: str): return (result, publish_status) first_result = run( - WithHandler(event_handler(), run_listener("a")), + event_handler()(run_listener("a")), handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) second_result = run( - WithHandler(event_handler(), run_listener("b")), + event_handler()(run_listener("b")), handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) diff --git a/packages/doeff-events/tests/test_publish.py b/packages/doeff-events/tests/test_publish.py index fec55dbf5..3025985e0 100644 --- a/packages/doeff-events/tests/test_publish.py +++ b/packages/doeff-events/tests/test_publish.py @@ -7,7 +7,7 @@ from doeff_events.effects import Publish, WaitForEvent from doeff_events.handlers import event_handler -from doeff import WithHandler, do, run +from doeff import do, run @dataclass(frozen=True) @@ -28,7 +28,7 @@ def program(): return "ok" result = run( - WithHandler(event_handler(), program()), + event_handler()(program()), handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) @@ -59,7 +59,7 @@ def program(): return (processed, producer_status) result = run( - WithHandler(event_handler(), program()), + event_handler()(program()), handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) diff --git a/packages/doeff-events/tests/test_typed_dispatch.py b/packages/doeff-events/tests/test_typed_dispatch.py index cae422cc4..e27106127 100644 --- a/packages/doeff-events/tests/test_typed_dispatch.py +++ b/packages/doeff-events/tests/test_typed_dispatch.py @@ -7,7 +7,7 @@ from doeff_events.effects import Publish, WaitForEvent from doeff_events.handlers import event_handler -from doeff import WithHandler, do, run +from doeff import do, run @dataclass(frozen=True) @@ -49,7 +49,7 @@ def program(): return (received, publish_status) result = run( - WithHandler(event_handler(), program()), + event_handler()(program()), handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) @@ -77,7 +77,7 @@ def program(): return (received, publish_status) result = run( - WithHandler(event_handler(), program()), + event_handler()(program()), handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) diff --git a/packages/doeff-events/tests/test_wait_for_event.py b/packages/doeff-events/tests/test_wait_for_event.py index ae7c21146..73eb717aa 100644 --- a/packages/doeff-events/tests/test_wait_for_event.py +++ b/packages/doeff-events/tests/test_wait_for_event.py @@ -7,7 +7,7 @@ from doeff_events.effects import Publish, WaitForEvent from doeff_events.handlers import event_handler -from doeff import WithHandler, do, run +from doeff import do, run @dataclass(frozen=True) @@ -33,7 +33,7 @@ def program(): return (received_event, publish_status) result = run( - WithHandler(event_handler(), program()), + event_handler()(program()), handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) diff --git a/packages/doeff-flow/examples/05_durable_execution.py b/packages/doeff-flow/examples/05_durable_execution.py index cd7e7d6e8..5e2ac8bf9 100644 --- a/packages/doeff-flow/examples/05_durable_execution.py +++ b/packages/doeff-flow/examples/05_durable_execution.py @@ -37,7 +37,6 @@ from doeff import ( Delegate, Resume, - WithHandler, do, slog, ) @@ -261,7 +260,7 @@ def main(): # Run the workflow with observability and custom cache effect handling db_path.parent.mkdir(parents=True, exist_ok=True) storage = SQLiteStorage(str(db_path)) - program = WithHandler(cache_handler(storage), durable_pipeline()) + program = cache_handler(storage)(durable_pipeline()) result = run_workflow( program, workflow_id="durable-demo", diff --git a/packages/doeff-flow/examples/06_testing_with_mocks.py b/packages/doeff-flow/examples/06_testing_with_mocks.py index 6fa5f13de..800cc0a73 100644 --- a/packages/doeff-flow/examples/06_testing_with_mocks.py +++ b/packages/doeff-flow/examples/06_testing_with_mocks.py @@ -19,7 +19,7 @@ from doeff_flow.handlers import MockTraceRecorder, mock_handlers from doeff_flow.trace import get_default_trace_dir -from doeff import WithHandler, default_handlers, do +from doeff import default_handlers, do from doeff import run as run_sync @@ -43,7 +43,7 @@ def main(): handlers = mock_handlers(recorder=recorder) trace_file = get_default_trace_dir() / workflow_id / "trace.jsonl" - wrapped = WithHandler(handlers, workflow_under_test()) + wrapped = handlers(workflow_under_test()) result = run_sync(wrapped, handlers=default_handlers()) if result.is_err(): raise RuntimeError(f"Workflow unexpectedly failed: {result.error!r}") diff --git a/packages/doeff-flow/src/doeff_flow/handlers/production.py b/packages/doeff-flow/src/doeff_flow/handlers/production.py index c36eca191..510ace051 100644 --- a/packages/doeff-flow/src/doeff_flow/handlers/production.py +++ b/packages/doeff-flow/src/doeff_flow/handlers/production.py @@ -9,6 +9,7 @@ from typing import Any from doeff import Effect, Pass, Resume, do +from doeff import handler as _program_handler from doeff_flow.effects import TraceAnnotate, TraceCapture, TracePush, TraceSnapshot from doeff_flow.trace import ( LiveTrace, @@ -163,7 +164,7 @@ def handler(effect: Effect, k: Any): return (yield Resume(k, captured)) return (yield Pass()) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-flow/src/doeff_flow/handlers/testing.py b/packages/doeff-flow/src/doeff_flow/handlers/testing.py index 8fa853546..e59d53495 100644 --- a/packages/doeff-flow/src/doeff_flow/handlers/testing.py +++ b/packages/doeff-flow/src/doeff_flow/handlers/testing.py @@ -8,6 +8,7 @@ from typing import Any from doeff import Effect, Pass, Resume, do +from doeff import handler as _program_handler from doeff_flow.effects import TraceAnnotate, TraceCapture, TracePush, TraceSnapshot ProtocolHandler = Callable[[Any, Any], Any] @@ -113,7 +114,7 @@ def handler(effect: Effect, k: Any): return (yield Resume(k, captured)) return (yield Pass()) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-flow/tests/test_e2e.py b/packages/doeff-flow/tests/test_e2e.py index 11a96f186..7a86513cc 100644 --- a/packages/doeff-flow/tests/test_e2e.py +++ b/packages/doeff-flow/tests/test_e2e.py @@ -10,7 +10,8 @@ from doeff_flow import run_workflow from doeff_flow.cli import cli -from doeff import Ask, Effect, Get, Pass, Pure, Put, WithHandler, async_run, default_handlers, do +from doeff import Ask, Effect, Get, Pass, Pure, Put, async_run, default_handlers, do +from doeff import handler as _install_raw_handler from doeff import run as run_sync @@ -138,7 +139,7 @@ def workflow(): return current + 1 result = run_sync( - WithHandler(capturing_handler, workflow()), + _install_raw_handler(capturing_handler)(workflow()), handlers=default_handlers(), store={}, ) @@ -165,7 +166,7 @@ def workflow(): return value * 2 result = await async_run( - WithHandler(capturing_handler, workflow()), + _install_raw_handler(capturing_handler)(workflow()), handlers=default_handlers(), env={"base": 15}, ) diff --git a/packages/doeff-flow/tests/test_effect_handlers.py b/packages/doeff-flow/tests/test_effect_handlers.py index 287c5a12f..15c4a34e0 100644 --- a/packages/doeff-flow/tests/test_effect_handlers.py +++ b/packages/doeff-flow/tests/test_effect_handlers.py @@ -7,7 +7,7 @@ from doeff_flow.effects import TraceAnnotate, TraceCapture, TracePush, TraceSnapshot from doeff_flow.handlers import mock_handlers, production_handlers -from doeff import WithHandler, default_handlers, do, run +from doeff import default_handlers, do, run def _read_entries(trace_dir: Path, workflow_id: str) -> list[dict]: @@ -18,7 +18,7 @@ def _read_entries(trace_dir: Path, workflow_id: str) -> list[dict]: def _run_with_handler(program, handler): return run( - WithHandler(handler, program), + handler(program), handlers=default_handlers(), ) diff --git a/packages/doeff-flow/tests/test_trace.py b/packages/doeff-flow/tests/test_trace.py index 2b16e48dd..5dc7ec870 100644 --- a/packages/doeff-flow/tests/test_trace.py +++ b/packages/doeff-flow/tests/test_trace.py @@ -15,7 +15,8 @@ write_terminal_trace, ) -from doeff import Ask, Effect, Get, Pass, Pure, Put, Resume, WithHandler, default_handlers, do +from doeff import Ask, Effect, Get, Pass, Pure, Put, Resume, default_handlers, do +from doeff import handler as _install_raw_handler from doeff import run as run_sync @@ -225,7 +226,7 @@ def workflow(): return current result = run_sync( - WithHandler(capturing_handler, workflow()), + _install_raw_handler(capturing_handler)(workflow()), handlers=default_handlers(), store={}, ) @@ -254,7 +255,7 @@ def workflow(): return a + b result = run_sync( - WithHandler(override_ask_handler, workflow()), + _install_raw_handler(override_ask_handler)(workflow()), handlers=default_handlers(), env={"a": 1, "b": 2}, ) diff --git a/packages/doeff-gemini/src/doeff_gemini/handlers/production.py b/packages/doeff-gemini/src/doeff_gemini/handlers/production.py index 5b7f32303..07d3c4c31 100644 --- a/packages/doeff-gemini/src/doeff_gemini/handlers/production.py +++ b/packages/doeff-gemini/src/doeff_gemini/handlers/production.py @@ -20,6 +20,7 @@ from PIL import Image from doeff import EffectBase, Pass, Resume, do +from doeff import handler as _program_handler from doeff_gemini.client import get_gemini_client, track_api_call from doeff_gemini.costs import calculate_known_model_cost from doeff_gemini.effects import ( @@ -475,7 +476,7 @@ def handler(effect: Any, k: Any): # noqa: PLR0911, PLR0912 - baseline cleanup k return (yield Resume(k, value)) yield Pass(effect, k) - return handler + return _program_handler(handler) @do diff --git a/packages/doeff-gemini/src/doeff_gemini/handlers/testing.py b/packages/doeff-gemini/src/doeff_gemini/handlers/testing.py index d34d37431..f342d0e4e 100644 --- a/packages/doeff-gemini/src/doeff_gemini/handlers/testing.py +++ b/packages/doeff-gemini/src/doeff_gemini/handlers/testing.py @@ -18,6 +18,7 @@ from PIL import Image as PILImage from doeff import Pass, Resume, do +from doeff import handler as _program_handler from doeff_gemini.effects import ( GeminiChat, GeminiEmbedding, @@ -215,7 +216,7 @@ def handler(effect: Any, k: Any): # noqa: PLR0911 - baseline cleanup keeps exis return (yield Resume(k, active_handler.handle_image_edit(effect))) yield Pass(effect, k) - return handler + return _program_handler(handler) @do diff --git a/packages/doeff-gemini/tests/e2e/test_gemini_live.py b/packages/doeff-gemini/tests/e2e/test_gemini_live.py index 351a25bdb..48fd3dc06 100644 --- a/packages/doeff-gemini/tests/e2e/test_gemini_live.py +++ b/packages/doeff-gemini/tests/e2e/test_gemini_live.py @@ -13,6 +13,8 @@ from PIL import Image from pydantic import BaseModel +from doeff import handler as _install_raw_handler + IMAGE_PACKAGE_ROOT = Path(__file__).resolve().parents[3] / "doeff-image" / "src" if str(IMAGE_PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(IMAGE_PACKAGE_ROOT)) @@ -32,7 +34,6 @@ EffectGenerator, Pass, Resume, - WithHandler, async_run, default_handlers, do, @@ -94,7 +95,7 @@ def mock_handler(effect: Effect, k: Any): return (yield Resume(k, "fake-gemini-key")) yield Pass() - return WithHandler(mock_handler, program) + return _install_raw_handler(mock_handler)(program) def _get_live_gemini_env_or_skip() -> dict[str, Any]: diff --git a/packages/doeff-gemini/tests/unit/test_effect_handlers.py b/packages/doeff-gemini/tests/unit/test_effect_handlers.py index 1b093d542..b9832f197 100644 --- a/packages/doeff-gemini/tests/unit/test_effect_handlers.py +++ b/packages/doeff-gemini/tests/unit/test_effect_handlers.py @@ -1,3 +1,4 @@ +from doeff import handler as _install_raw_handler # ruff: noqa: E402, I001 """Tests for Gemini effect and handler modules.""" @@ -62,7 +63,6 @@ def model_validate(cls, value: Any) -> Any: Delegate, EffectGenerator, Resume, - WithHandler, default_handlers, do, run, @@ -93,7 +93,7 @@ class FunFact(BaseModel): def _run_with_handler(program, handler): return run( - WithHandler(handler, program), + handler(program), handlers=default_handlers(), ) @@ -248,7 +248,7 @@ def program() -> EffectGenerator[str]: ) result = run( - WithHandler(fallback_handler, WithHandler(gemini_mock_handler, program())), + _install_raw_handler(fallback_handler)(gemini_mock_handler(program())), handlers=default_handlers(), ) diff --git a/packages/doeff-gemini/tests/unit/test_structured_llm.py b/packages/doeff-gemini/tests/unit/test_structured_llm.py index 539b29918..3aba89130 100644 --- a/packages/doeff-gemini/tests/unit/test_structured_llm.py +++ b/packages/doeff-gemini/tests/unit/test_structured_llm.py @@ -15,6 +15,8 @@ import pytest +from doeff import handler as _program_handler + IMAGE_PACKAGE_ROOT = Path(__file__).resolve().parents[3] / "doeff-image" / "src" if str(IMAGE_PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(IMAGE_PACKAGE_ROOT)) @@ -95,13 +97,12 @@ def handler(effect: MemoGetEffect | MemoPutEffect, k: Any) -> EffectGenerator[An return (yield Resume(k, None)) - return handler + return _program_handler(handler) async def _run_with_cache(program: Any, cache: _InMemoryTTLCache, *, env: dict[str, Any] | None = None): - from doeff import WithHandler - return await _run_with_default_cost(WithHandler(cache.make_handler(), program), env=env) + return await _run_with_default_cost(cache.make_handler()(program), env=env) def _extract_file_uri(part: Any) -> str | None: diff --git a/packages/doeff-git/src/doeff_git/handlers/production.py b/packages/doeff-git/src/doeff_git/handlers/production.py index badb05b6a..8739f43e1 100644 --- a/packages/doeff-git/src/doeff_git/handlers/production.py +++ b/packages/doeff-git/src/doeff_git/handlers/production.py @@ -8,6 +8,7 @@ from typing import Any from doeff import Effect, Pass, Resume, do +from doeff import handler as _program_handler from doeff_git.effects import CreatePR, GitCommit, GitDiff, GitPull, GitPush, MergePR from doeff_git.exceptions import GitCommandError from doeff_git.types import MergeStrategy, PRHandle @@ -203,7 +204,7 @@ def handler(effect: Effect, k: Any): # noqa: PLR0911 - baseline cleanup keeps e return (yield Resume(k, None)) return (yield Pass(effect, k)) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-git/src/doeff_git/handlers/testing.py b/packages/doeff-git/src/doeff_git/handlers/testing.py index e374454b6..2774e600e 100644 --- a/packages/doeff-git/src/doeff_git/handlers/testing.py +++ b/packages/doeff-git/src/doeff_git/handlers/testing.py @@ -7,6 +7,7 @@ from typing import Any from doeff import Effect, Pass, Resume, do +from doeff import handler as _program_handler from doeff_git.effects import CreatePR, GitCommit, GitDiff, GitPull, GitPush, MergePR from doeff_git.types import PRHandle @@ -111,7 +112,7 @@ def handler(effect: Effect, k: Any): # noqa: PLR0911 - baseline cleanup keeps e return (yield Resume(k, None)) return (yield Pass(effect, k)) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-git/tests/unit/test_effect_handlers.py b/packages/doeff-git/tests/unit/test_effect_handlers.py index 54bbb254b..d43c9c715 100644 --- a/packages/doeff-git/tests/unit/test_effect_handlers.py +++ b/packages/doeff-git/tests/unit/test_effect_handlers.py @@ -8,7 +8,7 @@ from pathlib import Path from unittest.mock import MagicMock, patch -from doeff import WithHandler, do, run +from doeff import do, run PACKAGE_ROOT = Path(__file__).resolve().parents[2] / "src" if str(PACKAGE_ROOT) not in sys.path: @@ -34,7 +34,7 @@ def is_ok(self) -> bool: def _run_with_handler(program, handler): - result = run(WithHandler(handler, program)) + result = run(handler(program)) if hasattr(result, "is_ok"): return result return _SuccessResult(result) diff --git a/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/handlers/production.py b/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/handlers/production.py index 803bc566e..9adc217b8 100644 --- a/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/handlers/production.py +++ b/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/handlers/production.py @@ -9,6 +9,7 @@ from doeff_secret.effects import DeleteSecret, GetSecret, ListSecrets, SetSecret from doeff import Effect, Pass, Resume, do +from doeff import handler as _program_handler from doeff_google_secret_manager.client import SecretManagerClient, get_secret_manager_client ProtocolHandler = Callable[[Any, Any], Any] @@ -159,7 +160,7 @@ def handler(effect: Effect, k: Any): return (yield runtime.handle_delete_secret(effect, k)) yield Pass(effect, k) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-google-secret-manager/tests/unit/test_effect_handlers.py b/packages/doeff-google-secret-manager/tests/unit/test_effect_handlers.py index df78a42a9..904076226 100644 --- a/packages/doeff-google-secret-manager/tests/unit/test_effect_handlers.py +++ b/packages/doeff-google-secret-manager/tests/unit/test_effect_handlers.py @@ -19,7 +19,7 @@ from doeff_google_secret_manager.handlers import mock_handlers, production_handlers # noqa: E402 from doeff_secret.effects import DeleteSecret, GetSecret, ListSecrets, SetSecret # noqa: E402 -from doeff import WithHandler, default_handlers, do, run # noqa: E402 +from doeff import default_handlers, do, run # noqa: E402 class AlreadyExistsError(Exception): @@ -89,7 +89,7 @@ def _is_err(run_result: Any) -> bool: def _run_with_handler(program, handler): return run( - WithHandler(handler, program), + handler(program), handlers=default_handlers(), ) diff --git a/packages/doeff-google-secret-manager/tests/unit/test_secrets.py b/packages/doeff-google-secret-manager/tests/unit/test_secrets.py index a315e5723..aa7c21dda 100644 --- a/packages/doeff-google-secret-manager/tests/unit/test_secrets.py +++ b/packages/doeff-google-secret-manager/tests/unit/test_secrets.py @@ -7,6 +7,8 @@ from types import SimpleNamespace from typing import Any +from doeff import handler as _install_raw_handler + PACKAGE_ROOT = Path(__file__).resolve().parents[2] / "src" if str(PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(PACKAGE_ROOT)) @@ -27,7 +29,6 @@ AskEffect, Pass, Resume, - WithHandler, default_handlers, do, run, @@ -106,7 +107,7 @@ def test_access_secret_returns_decoded_secret_with_mock_handler() -> None: mock_handler, _, _ = _build_handler(ask_values={"secret_manager_client": mock_client}) result = run( - WithHandler(mock_handler, access_secret("db-password")), + _install_raw_handler(mock_handler)(access_secret("db-password")), handlers=default_handlers(), ) @@ -123,10 +124,7 @@ def test_access_secret_can_return_bytes_with_decode_false() -> None: mock_handler, _, _ = _build_handler(ask_values={"secret_manager_client": mock_client}) result = run( - WithHandler( - mock_handler, - access_secret("binary-secret", decode=False, project="other-project"), - ), + _install_raw_handler(mock_handler)(access_secret("binary-secret", decode=False, project="other-project")), handlers=default_handlers(), ) @@ -144,7 +142,7 @@ def test_access_secret_uses_explicit_version_in_request_path() -> None: mock_handler, _, _ = _build_handler(ask_values={"secret_manager_client": mock_client}) result = run( - WithHandler(mock_handler, access_secret("api-key", version="42")), + _install_raw_handler(mock_handler)(access_secret("api-key", version="42")), handlers=default_handlers(), ) @@ -160,7 +158,7 @@ def test_access_secret_propagates_not_found_error() -> None: mock_handler, _, _ = _build_handler(ask_values={"secret_manager_client": mock_client}) result = run( - WithHandler(mock_handler, access_secret("missing-secret")), + _install_raw_handler(mock_handler)(access_secret("missing-secret")), handlers=default_handlers(), ) @@ -182,7 +180,7 @@ def test_get_secret_manager_client_initializes_and_caches_in_state() -> None: ) result = run( - WithHandler(mock_handler, get_secret_manager_client()), + _install_raw_handler(mock_handler)(get_secret_manager_client()), handlers=default_handlers(), ) @@ -208,7 +206,7 @@ def test_get_secret_manager_client_uses_cached_state_client_without_put() -> Non ) result = run( - WithHandler(mock_handler, get_secret_manager_client()), + _install_raw_handler(mock_handler)(get_secret_manager_client()), handlers=default_handlers(), ) diff --git a/packages/doeff-hy/src/doeff_hy/handle.hy b/packages/doeff-hy/src/doeff_hy/handle.hy index 99337e4b9..271fd9ed1 100644 --- a/packages/doeff-hy/src/doeff_hy/handle.hy +++ b/packages/doeff-hy/src/doeff_hy/handle.hy @@ -468,12 +468,13 @@ :when (pred x) (resume (+ x 1)))) - Wraps body with WithHandler. Unmatched effects auto-Pass. + Wraps body with the Rust handler node. Unmatched effects auto-Pass. Compile-time error if any clause branch lacks resume/transfer/pass." (setv h-expr (_build-handler-expr clauses)) `(do (import doeff.do [do :as _doeff-do]) - (import doeff [Resume Transfer Pass WithHandler]) + (import doeff [Resume Transfer Pass]) + (import doeff_vm [WithHandler]) (WithHandler ~h-expr ~body))) @@ -555,18 +556,14 @@ (import doeff_core_effects.effects [Get Put])) `(do))) - ;; New-style: defhandler produces a function (Program -> Program) instead - ;; of a raw handler dispatcher. The inner dispatcher (handler-expr) is kept - ;; as handler-data and wrapped in a thin function that calls WithHandler. - ;; - ;; The function is marked with _doeff_is_handler_fn=True so the public - ;; WithHandler(h, body) shim (in doeff/program.py) can detect it and route - ;; to h(body) — letting legacy code paths using (WithHandler my-h body) - ;; keep working during migration. + ;; defhandler produces a Program -> Program function instead of exposing a + ;; raw handler dispatcher. The inner dispatcher is stored for introspection + ;; and installed with the Rust VM WithHandler node. (if (is params None) `(do (import doeff.do [do :as _doeff-do]) - (import doeff [Resume Transfer Pass WithHandler]) + (import doeff [Resume Transfer Pass]) + (import doeff_vm [WithHandler]) ~lazy-imports (setv ~name ((fn [] @@ -582,7 +579,8 @@ (setv (. ~name __doeff_name__) ~(str name))) `(do (import doeff.do [do :as _doeff-do]) - (import doeff [Resume Transfer Pass WithHandler]) + (import doeff [Resume Transfer Pass]) + (import doeff_vm [WithHandler]) ~lazy-imports (defn ~name [~@params] (setv __doeff-handler-data__ ~handler-expr) diff --git a/packages/doeff-hy/tests/test_defpipeline.hy b/packages/doeff-hy/tests/test_defpipeline.hy index b31f28751..6355c2fa2 100644 --- a/packages/doeff-hy/tests/test_defpipeline.hy +++ b/packages/doeff-hy/tests/test_defpipeline.hy @@ -1,7 +1,7 @@ ;;; Test defpipeline macro (require doeff-hy.macros [defk defp <- defpipeline]) (import doeff [do :as _doeff-do]) -(import doeff [EffectBase K run WithHandler Resume Pass]) +(import doeff [EffectBase K run Resume Pass]) (import doeff_core_effects.scheduler [scheduled]) (import dataclasses [dataclass]) @@ -77,9 +77,7 @@ (defn run-with-mocks [program] (run (scheduled - (WithHandler mock-ohlc - (WithHandler mock-news - (WithHandler mock-llm program)))))) + (mock-ohlc (mock-news (mock-llm program)))))) ;; Run just ohlc stage (setv ohlc-result (run-with-mocks p-daily-cllm-ohlc)) diff --git a/packages/doeff-hy/tests/test_duplicate_stages.hy b/packages/doeff-hy/tests/test_duplicate_stages.hy index 518424558..97e251574 100644 --- a/packages/doeff-hy/tests/test_duplicate_stages.hy +++ b/packages/doeff-hy/tests/test_duplicate_stages.hy @@ -1,7 +1,7 @@ ;;; Test: duplicate bind names + index-based stage-of (require doeff-hy.macros [defk defp <-]) (import doeff [do :as _doeff-do]) -(import doeff [EffectBase K run WithHandler Resume Pass Pure]) +(import doeff [EffectBase K run Resume Pass Pure]) (import doeff_core_effects.scheduler [scheduled]) (import dataclasses [dataclass]) @@ -29,7 +29,7 @@ (yield (Resume k (+ "enriched:" (str effect.data)))) True (yield (Pass effect k)))) -(defn run-mock [p] (run (scheduled (WithHandler mock p)))) +(defn run-mock [p] (run (scheduled (mock p)))) ;; Tests (import doeff_hy.sexpr [list-stages stage-of]) diff --git a/packages/doeff-hy/tests/test_lazy_and_threading.py b/packages/doeff-hy/tests/test_lazy_and_threading.py index 2784eadb4..fb4066880 100644 --- a/packages/doeff-hy/tests/test_lazy_and_threading.py +++ b/packages/doeff-hy/tests/test_lazy_and_threading.py @@ -12,7 +12,8 @@ from doeff_core_effects.handlers import lazy_ask, state from doeff_core_effects.scheduler import scheduled -from doeff import EffectBase, Some, WithHandler, do +from doeff import EffectBase, Some, do +from doeff import handler as _install_raw_handler from doeff import run as doeff_run # ── Test effects ────────────────────────────────────────────────── @@ -38,7 +39,7 @@ def run_with(program, env=None, store=None): state(initial=store or {}), ] for h in reversed(handlers): - wrapped = WithHandler(h, wrapped) + wrapped = _install_raw_handler(h)(wrapped) wrapped = scheduled(wrapped) return doeff_run(wrapped) @@ -59,7 +60,7 @@ def test_lazy_handler_basic(self): code = """ (require doeff-hy.macros [defk defp <-]) (require doeff-hy.handle [defhandler]) -(import doeff [do :as _doeff-do EffectBase WithHandler Some Nothing run :as doeff-run]) +(import doeff [do :as _doeff-do EffectBase Some Nothing run :as doeff-run]) (import doeff_core_effects [Ask Get Put state]) (import doeff_core_effects.handlers [lazy-ask]) (import doeff_core_effects.scheduler [scheduled]) @@ -82,9 +83,9 @@ def test_lazy_handler_basic(self): [r1 r2]) (setv wrapped body) -(setv wrapped (WithHandler my-handler wrapped)) +(setv wrapped (my-handler wrapped)) (for [h (reversed [(lazy-ask :env {"endpoint" "localhost"}) (state)])] - (setv wrapped (WithHandler h wrapped))) + (setv wrapped (h wrapped))) (setv wrapped (scheduled wrapped)) (setv _result (doeff-run wrapped)) (assert (= _result ["client:localhost:a" "client:localhost:b"])) @@ -124,7 +125,7 @@ def body(): return [r1, r2] result = run_with( - WithHandler(handler_with_lazy, body()), + _install_raw_handler(handler_with_lazy)(body()), ) assert result == ["initialized-client:a", "initialized-client:b"] @@ -154,7 +155,7 @@ def body(): return [r1, r2] result = run_with( - WithHandler(handler_with_lazy_none, body()), + _install_raw_handler(handler_with_lazy_none)(body()), ) assert result == [None, None] assert init_count[0] == 1, "Init should run exactly once, not twice" @@ -194,7 +195,7 @@ def test_defhandler_lazy_with_effects(self): code = """ (require doeff-hy.macros [defp <-]) (require doeff-hy.handle [defhandler]) -(import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) +(import doeff [do :as _doeff-do EffectBase run :as doeff-run]) (import doeff_core_effects [Ask Get Put state]) (import doeff_core_effects.handlers [lazy-ask]) (import doeff_core_effects.scheduler [scheduled]) @@ -216,9 +217,9 @@ def test_defhandler_lazy_with_effects(self): [r1 r2]) (setv wrapped body) -(setv wrapped (WithHandler greet-handler wrapped)) +(setv wrapped (greet-handler wrapped)) (for [h (reversed [(lazy-ask :env {"prefix" "Hi"}) (state)])] - (setv wrapped (WithHandler h wrapped))) + (setv wrapped (h wrapped))) (setv wrapped (scheduled wrapped)) (setv __test_result__ (doeff-run wrapped)) """ @@ -258,7 +259,7 @@ def spy_state(effect, k): code = """ (require doeff-hy.macros [defp <-]) (require doeff-hy.handle [defhandler]) -(import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) +(import doeff [do :as _doeff-do EffectBase run :as doeff-run]) (import doeff_core_effects [Ask Get Put]) (import doeff_core_effects.handlers [lazy-ask]) (import doeff_core_effects.scheduler [scheduled]) @@ -282,8 +283,7 @@ def spy_state(effect, k): hy.eval(hy.read_many(code), module=mod) prog = mod.body - wrapped = WithHandler(spy_state, - WithHandler(mod.ping_handler2, prog)) + wrapped = _install_raw_handler(spy_state)(mod.ping_handler2(prog)) wrapped = scheduled(wrapped) doeff_run(wrapped) @@ -312,7 +312,7 @@ def test_defk_lazy_basic(self): code = """ (require doeff-hy.macros [defk defp <-]) -(import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) +(import doeff [do :as _doeff-do EffectBase run :as doeff-run]) (import doeff_core_effects [Ask Get Put state]) (import doeff_core_effects.handlers [lazy-ask]) (import doeff_core_effects.scheduler [scheduled]) @@ -331,7 +331,7 @@ def test_defk_lazy_basic(self): (setv wrapped body) (for [h (reversed [(lazy-ask :env {"prefix" "X"}) (state)])] - (setv wrapped (WithHandler h wrapped))) + (setv wrapped (h wrapped))) (setv wrapped (scheduled wrapped)) (setv __test_result__ (doeff-run wrapped)) """ @@ -362,7 +362,7 @@ def test_threading_expands_correctly(self): code = """ (require doeff-hy.macros [defk defp <- <->]) -(import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) +(import doeff [do :as _doeff-do EffectBase run :as doeff-run]) (import doeff_core_effects [Ask]) (import doeff_core_effects.handlers [lazy-ask]) (import doeff_core_effects.scheduler [scheduled]) @@ -502,7 +502,7 @@ def test_lazy_val_is_alias_for_lazy(self): code2 = """ (require doeff-hy.macros [defp <-]) (require doeff-hy.handle [defhandler]) -(import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) +(import doeff [do :as _doeff-do EffectBase run :as doeff-run]) (import doeff_core_effects [Get Put state]) (import doeff_core_effects.scheduler [scheduled]) (import dataclasses [dataclass]) @@ -518,8 +518,8 @@ def test_lazy_val_is_alias_for_lazy(self): r) (setv wrapped body) -(setv wrapped (WithHandler val-handler wrapped)) -(setv wrapped (WithHandler (state) wrapped)) +(setv wrapped (val-handler wrapped)) +(setv wrapped ((state) wrapped)) (setv wrapped (scheduled wrapped)) (setv __test_result__ (doeff-run wrapped)) """ @@ -535,7 +535,7 @@ def test_lazy_var_with_set_bang(self): code = """ (require doeff-hy.macros [defk defp <- set!]) (require doeff-hy.handle [defhandler]) -(import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) +(import doeff [do :as _doeff-do EffectBase run :as doeff-run]) (import doeff_core_effects [Get Put state]) (import doeff_core_effects.scheduler [scheduled]) (import dataclasses [dataclass]) @@ -558,8 +558,8 @@ def test_lazy_var_with_set_bang(self): [c0 c1 c2 c3]) (setv wrapped body) -(setv wrapped (WithHandler counter-handler wrapped)) -(setv wrapped (WithHandler (state) wrapped)) +(setv wrapped (counter-handler wrapped)) +(setv wrapped ((state) wrapped)) (setv wrapped (scheduled wrapped)) (setv __test_result__ (doeff-run wrapped)) """ @@ -575,7 +575,7 @@ def test_lazy_var_set_bang_persists_across_calls(self): code = """ (require doeff-hy.macros [defk defp <- set!]) (require doeff-hy.handle [defhandler]) -(import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run Some]) +(import doeff [do :as _doeff-do EffectBase run :as doeff-run Some]) (import doeff_core_effects [Get Put state]) (import doeff_core_effects.scheduler [scheduled]) (import dataclasses [dataclass]) @@ -599,8 +599,8 @@ def test_lazy_var_set_bang_persists_across_calls(self): result) (setv wrapped body) -(setv wrapped (WithHandler bag-handler wrapped)) -(setv wrapped (WithHandler (state) wrapped)) +(setv wrapped (bag-handler wrapped)) +(setv wrapped ((state) wrapped)) (setv wrapped (scheduled wrapped)) (setv __test_result__ (doeff-run wrapped)) """ @@ -637,7 +637,7 @@ def test_lazy_var_in_defk(self): import sys code = """ (require doeff-hy.macros [defk defp <- set!]) -(import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) +(import doeff [do :as _doeff-do EffectBase run :as doeff-run]) (import doeff_core_effects [Get Put state]) (import doeff_core_effects.scheduler [scheduled]) @@ -653,7 +653,7 @@ def test_lazy_var_in_defk(self): [r1 r2]) (setv wrapped body) -(setv wrapped (WithHandler (state) wrapped)) +(setv wrapped ((state) wrapped)) (setv wrapped (scheduled wrapped)) (setv __test_result__ (doeff-run wrapped)) """ @@ -671,7 +671,7 @@ def test_lazy_val_referenced_inside_tuple(self): code = """ (require doeff-hy.macros [defp <-]) (require doeff-hy.handle [defhandler]) -(import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) +(import doeff [do :as _doeff-do EffectBase run :as doeff-run]) (import doeff_core_effects [Ask Get Put state]) (import doeff_core_effects.handlers [lazy-ask]) (import doeff_core_effects.scheduler [scheduled]) @@ -692,9 +692,9 @@ def test_lazy_val_referenced_inside_tuple(self): r) (setv wrapped body) -(setv wrapped (WithHandler pair-handler wrapped)) +(setv wrapped (pair-handler wrapped)) (for [h (reversed [(lazy-ask :env {"session_id" "s1"}) (state)])] - (setv wrapped (WithHandler h wrapped))) + (setv wrapped (h wrapped))) (setv wrapped (scheduled wrapped)) (setv __test_result__ (doeff-run wrapped)) """ diff --git a/packages/doeff-hy/tests/test_multimod.hy b/packages/doeff-hy/tests/test_multimod.hy index c4b2a3c68..478af6741 100644 --- a/packages/doeff-hy/tests/test_multimod.hy +++ b/packages/doeff-hy/tests/test_multimod.hy @@ -125,7 +125,7 @@ (print "Part 3: Compile and run transformed program") (print (* "=" 60)) -(import doeff [run WithHandler Pure Resume Pass]) +(import doeff [run Pure Resume Pass]) (import doeff [do :as _doeff-do]) (import doeff [EffectBase K]) (import doeff_core_effects.scheduler [scheduled]) @@ -164,9 +164,7 @@ ;; Run it with mock handlers (setv program - (WithHandler mock-price-handler - (WithHandler mock-news-handler - (compiled-fn "7203.T" "2025-11-10")))) + (mock-price-handler (mock-news-handler (compiled-fn "7203.T" "2025-11-10")))) (print) (print "--- Running transformed program ---") diff --git a/packages/doeff-hy/tests/test_stage_of.hy b/packages/doeff-hy/tests/test_stage_of.hy index b8b27df59..2908936ae 100644 --- a/packages/doeff-hy/tests/test_stage_of.hy +++ b/packages/doeff-hy/tests/test_stage_of.hy @@ -1,7 +1,7 @@ ;;; Test stage-of: partial pipeline reuse (require doeff-hy.macros [defk defp <-]) (import doeff [do :as _doeff-do]) -(import doeff [EffectBase K run WithHandler Resume Pass]) +(import doeff [EffectBase K run Resume Pass]) (import doeff_core_effects.scheduler [scheduled]) (import dataclasses [dataclass]) @@ -59,8 +59,7 @@ (defn run-with-mocks [program] (run (scheduled - (WithHandler mock-ohlc - (WithHandler mock-news program))))) + (mock-ohlc (mock-news program))))) ;; === Test list-stages === diff --git a/packages/doeff-image/tests/test_multi_provider_workflow.py b/packages/doeff-image/tests/test_multi_provider_workflow.py index e3745e9c0..ed43d73ce 100644 --- a/packages/doeff-image/tests/test_multi_provider_workflow.py +++ b/packages/doeff-image/tests/test_multi_provider_workflow.py @@ -48,7 +48,7 @@ class ValidationError(Exception): # type: ignore[no-redef] from doeff_image.effects import ImageEdit, ImageGenerate from doeff_image.types import ImageResult -from doeff import Delegate, EffectGenerator, Resume, WithHandler, default_handlers, do, run +from doeff import Delegate, EffectGenerator, Resume, default_handlers, do, run def _fallback_handler(value: str): @@ -71,10 +71,7 @@ def flow() -> EffectGenerator[str]: ) result = run( - WithHandler( - seedream_production.seedream_image_handler, - WithHandler(_fallback_handler("delegated"), flow()), - ), + seedream_production.seedream_image_handler(_fallback_handler("delegated")(flow())), handlers=default_handlers(), ) assert result.is_ok() @@ -92,10 +89,7 @@ def flow() -> EffectGenerator[str]: ) result = run( - WithHandler( - gemini_production.gemini_image_handler, - WithHandler(_fallback_handler("delegated"), flow()), - ), + gemini_production.gemini_image_handler(_fallback_handler("delegated")(flow())), handlers=default_handlers(), ) assert result.is_ok() @@ -143,10 +137,7 @@ def flow() -> EffectGenerator[tuple[ImageResult, ImageResult]]: return base, edited result = run( - WithHandler( - seedream_production.seedream_image_handler, - WithHandler(gemini_production.gemini_image_handler, flow()), - ), + seedream_production.seedream_image_handler(gemini_production.gemini_image_handler(flow())), handlers=default_handlers(), ) diff --git a/packages/doeff-ml-nexus/src/doeff_ml_nexus/runner.hy b/packages/doeff-ml-nexus/src/doeff_ml_nexus/runner.hy index 873e06300..c53360daf 100644 --- a/packages/doeff-ml-nexus/src/doeff_ml_nexus/runner.hy +++ b/packages/doeff-ml-nexus/src/doeff_ml_nexus/runner.hy @@ -8,7 +8,7 @@ (require doeff_hy.macros [defk defp <-]) (require doeff_docker.compose [with-handlers]) (import doeff [do :as _doeff-do]) -(import doeff [run WithHandler]) +(import doeff [run]) (import doeff_core_effects [Ask reader slog-handler writer scheduled]) (import pathlib [Path]) diff --git a/packages/doeff-ml-nexus/src/doeff_ml_nexus/stack.hy b/packages/doeff-ml-nexus/src/doeff_ml_nexus/stack.hy index c2d118b78..aae0acf4f 100644 --- a/packages/doeff-ml-nexus/src/doeff_ml_nexus/stack.hy +++ b/packages/doeff-ml-nexus/src/doeff_ml_nexus/stack.hy @@ -4,7 +4,7 @@ (require doeff_hy.macros [defk <-]) (require doeff_docker.compose [with-handlers]) (import doeff [do :as _doeff-do]) -(import doeff [run WithHandler]) +(import doeff [run]) (import doeff_core_effects [reader writer slog-handler scheduled]) (import doeff_ml_nexus.serializer [default-serializer]) diff --git a/packages/doeff-ml-nexus/tests/test_docker.py b/packages/doeff-ml-nexus/tests/test_docker.py index 593f3ae64..3f57e1ccd 100644 --- a/packages/doeff-ml-nexus/tests/test_docker.py +++ b/packages/doeff-ml-nexus/tests/test_docker.py @@ -2,7 +2,7 @@ import hy # noqa: F401 - activates Hy import hooks for test modules -from doeff import run, WithHandler, do +from doeff import run, do from doeff_core_effects import reader, writer, slog_handler from doeff_docker.handlers.dockerfile import collect_dockerfile @@ -11,10 +11,7 @@ def _run_with_handlers(program): return run( - WithHandler(writer(), - WithHandler(slog_handler(), - WithHandler(reader(env={}), - program))) + writer()(slog_handler()(reader(env={})(program))) ) diff --git a/packages/doeff-notify/tests/test_handlers.py b/packages/doeff-notify/tests/test_handlers.py index 0b0b6e175..13f9da28f 100644 --- a/packages/doeff-notify/tests/test_handlers.py +++ b/packages/doeff-notify/tests/test_handlers.py @@ -1,3 +1,5 @@ +from doeff import handler as _install_raw_handler + # ruff: noqa: E402 """Tests for doeff-notify effects and built-in handlers.""" @@ -22,7 +24,7 @@ ) from doeff_notify.types import Channel, NotificationResult, Urgency -from doeff import Effect, Pass, Resume, WithHandler, WriterTellEffect, default_handlers, do, run +from doeff import Effect, Pass, Resume, WriterTellEffect, default_handlers, do, run def _is_ok(run_result: Any) -> bool: @@ -58,7 +60,7 @@ def _console_program(): def test_console_handler_prints_and_returns_notification_result(capsys) -> None: result = run( - WithHandler(console_handler, _console_program()), + console_handler(_console_program()), handlers=default_handlers(), ) @@ -88,7 +90,7 @@ def test_testing_handler_collects_notifications_in_memory() -> None: handler, notifications = build_testing_handler(auto_acknowledge=True) result = run( - WithHandler(handler, _testing_program()), + handler(_testing_program()), handlers=default_handlers(), ) @@ -138,7 +140,7 @@ def capture_tell_handler(effect: Effect, k: Any): yield Pass() result = run( - WithHandler(capture_tell_handler, WithHandler(log_handler, _logging_program())), + _install_raw_handler(capture_tell_handler)(log_handler(_logging_program())), handlers=default_handlers(), ) diff --git a/packages/doeff-openai/src/doeff_openai/handlers/testing.py b/packages/doeff-openai/src/doeff_openai/handlers/testing.py index bff6919b5..ec846bb78 100644 --- a/packages/doeff-openai/src/doeff_openai/handlers/testing.py +++ b/packages/doeff-openai/src/doeff_openai/handlers/testing.py @@ -14,6 +14,7 @@ ) from doeff import Pass, Resume, do +from doeff import handler as _program_handler from doeff_openai.effects import ( ChatCompletion, Embedding, @@ -425,7 +426,7 @@ def handler(effect: Any, k: Any): ) ) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-openai/tests/_runner.py b/packages/doeff-openai/tests/_runner.py index 26ad94226..33db57beb 100644 --- a/packages/doeff-openai/tests/_runner.py +++ b/packages/doeff-openai/tests/_runner.py @@ -35,7 +35,7 @@ from doeff_openai.handlers import calculate_cost_handler from doeff_vm import Err, Ok -from doeff import AskEffect, Pass, Resume, WithHandler, do, run +from doeff import AskEffect, Pass, Resume, do, run @dataclass @@ -74,15 +74,15 @@ def _build_chain(program: Any, env: dict | None): writer_h = writer() slog_h = slog_handler() wrapped = program - wrapped = WithHandler(calculate_cost_handler, wrapped) - wrapped = WithHandler(await_handler(), wrapped) - wrapped = WithHandler(listen_handler, wrapped) - wrapped = WithHandler(local_handler, wrapped) - wrapped = WithHandler(state(), wrapped) - wrapped = WithHandler(try_handler, wrapped) - wrapped = WithHandler(writer_h, wrapped) - wrapped = WithHandler(slog_h, wrapped) - wrapped = WithHandler(lazy_ask(env=env or {}), wrapped) + wrapped = calculate_cost_handler(wrapped) + wrapped = await_handler()(wrapped) + wrapped = listen_handler(wrapped) + wrapped = local_handler(wrapped) + wrapped = state()(wrapped) + wrapped = try_handler(wrapped) + wrapped = writer_h(wrapped) + wrapped = slog_h(wrapped) + wrapped = lazy_ask(env=env or {})(wrapped) return scheduled(wrapped), writer_h, slog_h diff --git a/packages/doeff-openai/tests/e2e/test_effect_handlers_e2e.py b/packages/doeff-openai/tests/e2e/test_effect_handlers_e2e.py index 05fea306a..00c89b78a 100644 --- a/packages/doeff-openai/tests/e2e/test_effect_handlers_e2e.py +++ b/packages/doeff-openai/tests/e2e/test_effect_handlers_e2e.py @@ -14,7 +14,7 @@ from doeff_openai.handlers import production_handlers from pydantic import BaseModel -from doeff import EffectGenerator, WithHandler, do +from doeff import EffectGenerator, do pytestmark = pytest.mark.e2e @@ -37,9 +37,7 @@ async def _async_run_with_handler(program, handler): environment variables. """ return await run_program( - WithHandler( - openai_api_key_from_doeff_py_handler, WithHandler(handler, program) - ) + openai_api_key_from_doeff_py_handler(handler(program)) ) diff --git a/packages/doeff-openai/tests/e2e/test_openai_api.py b/packages/doeff-openai/tests/e2e/test_openai_api.py index 76d9991d3..dc4d0a240 100644 --- a/packages/doeff-openai/tests/e2e/test_openai_api.py +++ b/packages/doeff-openai/tests/e2e/test_openai_api.py @@ -33,9 +33,9 @@ EffectGenerator, Pass, Resume, - WithHandler, do, ) +from doeff import handler as _program_handler # Mark all tests in this module as e2e pytestmark = pytest.mark.e2e @@ -128,14 +128,14 @@ def mock_handler(effect: Effect, k: Any): return (yield Resume(k, "sk-fake-test-key")) yield Pass(effect, k) - return mock_handler + return _program_handler(mock_handler) async def run_with_mock_handler(program: Any, mock_create: AsyncMock): """Run a program with handler-provided OpenAI client mocks.""" mock_client = _make_mock_client(mock_create) result = await run_program( - WithHandler(_make_mock_handler(mock_client), program) + _make_mock_handler(mock_client)(program) ) return result @@ -538,7 +538,7 @@ def test_program() -> EffectGenerator[str]: return result result = await run_program( - WithHandler(openai_api_key_from_doeff_py_handler, test_program()), + openai_api_key_from_doeff_py_handler(test_program()), ) assert result.is_ok() @@ -566,7 +566,7 @@ def test_program() -> EffectGenerator[MathAnswer]: return result result = await run_program( - WithHandler(openai_api_key_from_doeff_py_handler, test_program()), + openai_api_key_from_doeff_py_handler(test_program()), ) assert result.is_ok() diff --git a/packages/doeff-openai/tests/unit/test_effect_handlers.py b/packages/doeff-openai/tests/unit/test_effect_handlers.py index e6f724922..a50cfd5c4 100644 --- a/packages/doeff-openai/tests/unit/test_effect_handlers.py +++ b/packages/doeff-openai/tests/unit/test_effect_handlers.py @@ -36,9 +36,9 @@ EffectGenerator, Pass, Resume, - WithHandler, do, ) +from doeff import handler as _install_raw_handler class StructuredAnswer(BaseModel): @@ -47,7 +47,7 @@ class StructuredAnswer(BaseModel): async def _async_run_with_handler(program, handler): - return await run_program(WithHandler(handler, program)) + return await run_program(handler(program)) async def _collect_stream_text(stream: Any) -> str: @@ -209,7 +209,7 @@ def flow() -> EffectGenerator[str]: ) result = await run_program( - WithHandler(fallback_handler, WithHandler(wrapped_openai_handler, flow())) + _install_raw_handler(fallback_handler)(_install_raw_handler(wrapped_openai_handler)(flow())) ) assert result.is_ok() diff --git a/packages/doeff-openai/tests/unit/test_pricing.py b/packages/doeff-openai/tests/unit/test_pricing.py index 8468cfeb1..51b139548 100644 --- a/packages/doeff-openai/tests/unit/test_pricing.py +++ b/packages/doeff-openai/tests/unit/test_pricing.py @@ -3,7 +3,7 @@ These tests do not depend on the legacy ``default_handlers`` helper (removed in an earlier refactor) — they compose handlers explicitly -with :class:`doeff.WithHandler`, mirroring the supported usage pattern. +by calling Program -> Program handlers directly. """ import pytest @@ -18,7 +18,8 @@ ) from doeff_openai.handlers.production import openai_production_handler -from doeff import Pass, Resume, WithHandler, do, run +from doeff import Pass, Resume, do, run +from doeff import handler as _install_raw_handler # --------------------------------------------------------------------------- # Pricing table — GPT-5 family exists and matches published rates @@ -123,7 +124,7 @@ def _ask_cost(model, usage): def test_calculate_cost_effect_known_model_resumes_with_cost_info(): usage = TokenUsage(prompt_tokens=1000, completion_tokens=500, total_tokens=1500) - result = run(WithHandler(openai_production_handler, _ask_cost("gpt-5-mini", usage))) + result = run(openai_production_handler(_ask_cost("gpt-5-mini", usage))) assert isinstance(result, CostInfo) assert result.model == "gpt-5-mini" assert result.total_cost > 0 @@ -144,10 +145,7 @@ def zero_cost_override(effect, k): usage = TokenUsage(prompt_tokens=100, completion_tokens=50, total_tokens=150) result = run( - WithHandler( - zero_cost_override, - WithHandler(openai_production_handler, _ask_cost("future-model-v9", usage)), - ) + _install_raw_handler(zero_cost_override)(openai_production_handler(_ask_cost("future-model-v9", usage))) ) assert result.total_cost == 0.0 assert result.model == "future-model-v9" @@ -159,7 +157,7 @@ def test_calculate_cost_effect_unknown_model_no_outer_handler_raises(): # is the loud-fail property: silent fall-back is impossible. usage = TokenUsage(prompt_tokens=100, completion_tokens=50, total_tokens=150) with pytest.raises(RuntimeError, match="no handler found for effect"): - run(WithHandler(openai_production_handler, _ask_cost("future-model-v9", usage))) + run(openai_production_handler(_ask_cost("future-model-v9", usage))) def test_calculate_cost_effect_cached_on_legacy_model_passes(): @@ -172,7 +170,7 @@ def test_calculate_cost_effect_cached_on_legacy_model_passes(): cached_prompt_tokens=100, ) with pytest.raises(RuntimeError, match="no handler found for effect"): - run(WithHandler(openai_production_handler, _ask_cost("gpt-3.5-turbo", usage))) + run(openai_production_handler(_ask_cost("gpt-3.5-turbo", usage))) # --------------------------------------------------------------------------- diff --git a/packages/doeff-openai/tests/unit/test_structured_llm.py b/packages/doeff-openai/tests/unit/test_structured_llm.py index b8b36d8dc..10b851dc1 100644 --- a/packages/doeff-openai/tests/unit/test_structured_llm.py +++ b/packages/doeff-openai/tests/unit/test_structured_llm.py @@ -30,9 +30,9 @@ EffectGenerator, Pass, Resume, - WithHandler, do, ) +from doeff import handler as _install_raw_handler # Test models for structured output @@ -569,7 +569,7 @@ def test_flow() -> EffectGenerator[SimpleResponse]: ) return result - result = await run_program(WithHandler(mock_openai_handler, test_flow())) + result = await run_program(_install_raw_handler(mock_openai_handler)(test_flow())) assert result.is_ok() assert isinstance(result.value, SimpleResponse) diff --git a/packages/doeff-openrouter/src/doeff_openrouter/handlers/testing.py b/packages/doeff-openrouter/src/doeff_openrouter/handlers/testing.py index 70f24a75a..872a2c04f 100644 --- a/packages/doeff-openrouter/src/doeff_openrouter/handlers/testing.py +++ b/packages/doeff-openrouter/src/doeff_openrouter/handlers/testing.py @@ -14,6 +14,7 @@ ) from doeff import Pass, Resume, do +from doeff import handler as _program_handler from doeff_openrouter.effects import ( RouterChat, RouterStreamingChat, @@ -87,7 +88,7 @@ def mock_handlers( def handler(effect: Any, k: Any): return (yield openrouter_mock_handler(effect, k, runtime=active_runtime)) - return handler + return _program_handler(handler) @do diff --git a/packages/doeff-openrouter/tests/e2e/test_openrouter_integration.py b/packages/doeff-openrouter/tests/e2e/test_openrouter_integration.py index 11e425d7b..c6a60d24f 100644 --- a/packages/doeff-openrouter/tests/e2e/test_openrouter_integration.py +++ b/packages/doeff-openrouter/tests/e2e/test_openrouter_integration.py @@ -23,11 +23,11 @@ EffectGenerator, Pass, Resume, - WithHandler, default_handlers, do, run, ) +from doeff import handler as _program_handler MOCK_STRUCTURED_MODELS = [ pytest.param("openai/gpt-4o-mini", True, id="openai-gpt-4o-mini"), @@ -107,7 +107,7 @@ def handler(effect: Effect, k: Any): return (yield Resume(k, "fake-key")) yield Pass() - return handler + return _program_handler(handler) @pytest.fixture(scope="module") @@ -148,7 +148,7 @@ def flow() -> EffectGenerator[Any]: return raw_response raw_result = run( - WithHandler(_build_mock_handler(mock_client), flow()), + _build_mock_handler(mock_client)(flow()), handlers=handlers, store={"openrouter_api_calls": []}, ) diff --git a/packages/doeff-openrouter/tests/unit/test_effect_handlers.py b/packages/doeff-openrouter/tests/unit/test_effect_handlers.py index b4712096b..202e817af 100644 --- a/packages/doeff-openrouter/tests/unit/test_effect_handlers.py +++ b/packages/doeff-openrouter/tests/unit/test_effect_handlers.py @@ -27,7 +27,8 @@ ) from pydantic import BaseModel -from doeff import Delegate, Resume, WithHandler, default_handlers, do, run +from doeff import Delegate, Resume, default_handlers, do, run +from doeff import handler as _install_raw_handler class StructuredPayload(BaseModel): @@ -37,7 +38,7 @@ class StructuredPayload(BaseModel): def _run_with_handler(program, handler): return run( - WithHandler(handler, program), + handler(program), handlers=default_handlers(), ) @@ -213,7 +214,7 @@ def workflow(): return (yield LLMEmbedding(input="hello", model="text-embedding-3-small")) result = run( - WithHandler(fallback, WithHandler(openrouter_mock_handler, workflow())), + _install_raw_handler(fallback)(openrouter_mock_handler(workflow())), handlers=default_handlers(), ) diff --git a/packages/doeff-preset/examples/01_basic_slog.py b/packages/doeff-preset/examples/01_basic_slog.py index 73d35e2d0..a7bd6dabb 100644 --- a/packages/doeff-preset/examples/01_basic_slog.py +++ b/packages/doeff-preset/examples/01_basic_slog.py @@ -12,7 +12,7 @@ from doeff_preset import preset_handlers -from doeff import WithHandler, default_handlers, do, run, slog +from doeff import default_handlers, do, run, slog @do @@ -44,7 +44,7 @@ def main(): print("=== Basic slog Display Example ===\n") result = run( - WithHandler(preset_handlers(), basic_workflow()), + preset_handlers()(basic_workflow()), handlers=default_handlers(), ) diff --git a/packages/doeff-preset/examples/02_configuration.py b/packages/doeff-preset/examples/02_configuration.py index 607e2583b..246062116 100644 --- a/packages/doeff-preset/examples/02_configuration.py +++ b/packages/doeff-preset/examples/02_configuration.py @@ -12,7 +12,7 @@ from doeff_preset import preset_handlers -from doeff import Ask, WithHandler, default_handlers, do, run, slog +from doeff import Ask, default_handlers, do, run, slog @do @@ -47,7 +47,7 @@ def main(): # Example 1: Default configuration print("=== Default Configuration ===\n") result = run( - WithHandler(preset_handlers(), configurable_workflow()), + preset_handlers()(configurable_workflow()), handlers=default_handlers(), ) print(f"\nConfig: {result.value}") @@ -62,7 +62,7 @@ def main(): } ) result2 = run( - WithHandler(custom_handlers, configurable_workflow()), + custom_handlers(configurable_workflow()), handlers=default_handlers(), ) print(f"\nConfig: {result2.value}") diff --git a/packages/doeff-preset/examples/03_merge_handlers.py b/packages/doeff-preset/examples/03_merge_handlers.py index a0419d140..40486f34b 100644 --- a/packages/doeff-preset/examples/03_merge_handlers.py +++ b/packages/doeff-preset/examples/03_merge_handlers.py @@ -14,7 +14,8 @@ from doeff_preset import preset_handlers -from doeff import Delegate, EffectBase, Resume, WithHandler, default_handlers, do, run, slog +from doeff import Delegate, EffectBase, Resume, default_handlers, do, run, slog +from doeff import handler as _install_raw_handler # Define a custom domain effect @@ -86,16 +87,7 @@ def main(): print("=== Merging Handlers Example ===\n") # Stack handlers explicitly: inner handlers shadow outer handlers. - stacked_program = WithHandler( - preset_handlers(), - WithHandler( - handle_fetch_user, - WithHandler( - handle_send_email, - notification_workflow(user_id=1, message="Hello from doeff!"), - ), - ), - ) + stacked_program = preset_handlers()(_install_raw_handler(handle_fetch_user)(_install_raw_handler(handle_send_email)(notification_workflow(user_id=1, message="Hello from doeff!")))) result = run( stacked_program, handlers=default_handlers(), diff --git a/packages/doeff-preset/examples/04_granular_handlers.py b/packages/doeff-preset/examples/04_granular_handlers.py index 7b3872957..1653af67d 100644 --- a/packages/doeff-preset/examples/04_granular_handlers.py +++ b/packages/doeff-preset/examples/04_granular_handlers.py @@ -12,7 +12,7 @@ from doeff_preset import config_handlers, log_display_handlers -from doeff import Ask, WithHandler, default_handlers, do, run, slog +from doeff import Ask, default_handlers, do, run, slog @do @@ -37,7 +37,7 @@ def main(): # Example 1: Only slog display (no config) print("=== Only Log Display Handlers ===\n") result1 = run( - WithHandler(log_display_handlers(), workflow_with_slog()), + log_display_handlers()(workflow_with_slog()), handlers=default_handlers(), ) print(f"Result: {result1.value}") @@ -47,7 +47,7 @@ def main(): # Note: Without log_display_handlers, slog still works but won't show rich output print("\n=== Only Config Handlers ===\n") result2 = run( - WithHandler(config_handlers(), workflow_with_config()), + config_handlers()(workflow_with_config()), handlers=default_handlers(), ) print(f"Config values: {result2.value}") @@ -69,7 +69,7 @@ def combined_workflow(): return config result3 = run( - WithHandler(log_display_handlers(), WithHandler(custom_config, combined_workflow())), + log_display_handlers()(custom_config(combined_workflow())), handlers=default_handlers(), ) print(f"Log level used: {result3.value}") diff --git a/packages/doeff-preset/examples/05_async_runtime.py b/packages/doeff-preset/examples/05_async_runtime.py index 8323e0a71..3d581f5d4 100644 --- a/packages/doeff-preset/examples/05_async_runtime.py +++ b/packages/doeff-preset/examples/05_async_runtime.py @@ -15,7 +15,6 @@ from doeff import ( Ask, - WithHandler, async_run, default_async_handlers, default_handlers, @@ -49,7 +48,7 @@ async def main(): # Run with run() print("=== Running with run() ===\n") sync_result = run( - WithHandler(handlers, async_workflow()), + handlers(async_workflow()), handlers=default_handlers(), ) print(f"\nSync result: {sync_result.value}") @@ -58,7 +57,7 @@ async def main(): # Run with async_run() print("\n=== Running with async_run() ===\n") async_result = await async_run( - WithHandler(handlers, async_workflow()), + handlers(async_workflow()), handlers=default_async_handlers(), ) print(f"\nAsync result: {async_result.value}") diff --git a/packages/doeff-preset/src/doeff_preset/__init__.py b/packages/doeff-preset/src/doeff_preset/__init__.py index f3ac78d06..b532df4f3 100644 --- a/packages/doeff-preset/src/doeff_preset/__init__.py +++ b/packages/doeff-preset/src/doeff_preset/__init__.py @@ -6,7 +6,7 @@ Example: >>> from doeff import do, slog - >>> from doeff import WithHandler, default_handlers, run + >>> from doeff import default_handlers, run >>> from doeff_preset import preset_handlers >>> >>> @do @@ -17,7 +17,7 @@ ... return "success" >>> >>> result = run( - ... WithHandler(preset_handlers(), my_workflow()), + ... preset_handlers()(my_workflow()), ... handlers=default_handlers(), ... ) >>> # slog messages are displayed to console AND accumulated in log diff --git a/packages/doeff-preset/src/doeff_preset/handlers/config.py b/packages/doeff-preset/src/doeff_preset/handlers/config.py index dac4dab5c..716f8c5f4 100644 --- a/packages/doeff-preset/src/doeff_preset/handlers/config.py +++ b/packages/doeff-preset/src/doeff_preset/handlers/config.py @@ -8,6 +8,7 @@ from typing import Any from doeff import AskEffect, Effect, MissingEnvKeyError, Pass, Resume, do +from doeff import handler as _program_handler from doeff_preset.effects.config import is_preset_config_key ProtocolHandler = Callable[[Any, Any], Any] @@ -63,7 +64,7 @@ def handle_ask_with_config(effect: Effect, k: Any): yield Pass() - return handle_ask_with_config + return _program_handler(handle_ask_with_config) def config_handlers(defaults: dict[str, Any] | None = None) -> ProtocolHandler: diff --git a/packages/doeff-preset/src/doeff_preset/handlers/log_display.py b/packages/doeff-preset/src/doeff_preset/handlers/log_display.py index 08e1fc395..c71eb41dc 100644 --- a/packages/doeff-preset/src/doeff_preset/handlers/log_display.py +++ b/packages/doeff-preset/src/doeff_preset/handlers/log_display.py @@ -13,6 +13,7 @@ from rich.text import Text from doeff import Effect, Pass, WriterTellEffect, do +from doeff import handler as _program_handler # Global console for log output _console = Console(stderr=True) @@ -109,7 +110,7 @@ def log_display_handlers() -> ProtocolHandler: def handler(effect: Effect, k: Any): return (yield handle_tell_with_display(effect, k)) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-preset/src/doeff_preset/handlers/production.py b/packages/doeff-preset/src/doeff_preset/handlers/production.py index d64a3e864..957a9f3b5 100644 --- a/packages/doeff-preset/src/doeff_preset/handlers/production.py +++ b/packages/doeff-preset/src/doeff_preset/handlers/production.py @@ -5,6 +5,7 @@ from typing import Any from doeff import AskEffect, Effect, Pass, WriterTellEffect, do +from doeff import handler as _program_handler from doeff_preset.handlers.config import config_handlers from doeff_preset.handlers.log_display import log_display_handlers @@ -24,7 +25,7 @@ def handler(effect: Effect, k: Any): return (yield ask_handler(effect, k)) yield Pass() - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-preset/src/doeff_preset/handlers/testing.py b/packages/doeff-preset/src/doeff_preset/handlers/testing.py index 77e7b62f0..c4b24efd1 100644 --- a/packages/doeff-preset/src/doeff_preset/handlers/testing.py +++ b/packages/doeff-preset/src/doeff_preset/handlers/testing.py @@ -5,6 +5,7 @@ from typing import Any from doeff import AskEffect, Effect, Pass, WriterTellEffect, do +from doeff import handler as _program_handler from doeff_preset.handlers.config import config_handlers ProtocolHandler = Callable[[Any, Any], Any] @@ -38,7 +39,7 @@ def handler(effect: Effect, k: Any): return (yield ask_handler(effect, k)) yield Pass() - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-preset/tests/test_preset_handlers.py b/packages/doeff-preset/tests/test_preset_handlers.py index eea2b7b60..214c3e210 100644 --- a/packages/doeff-preset/tests/test_preset_handlers.py +++ b/packages/doeff-preset/tests/test_preset_handlers.py @@ -7,6 +7,8 @@ import pytest +from doeff import handler as _install_raw_handler + sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from doeff_preset import ( @@ -39,7 +41,6 @@ Pass, Resume, Tell, - WithHandler, WriterTellEffect, async_run, default_async_handlers, @@ -54,7 +55,7 @@ def _run_with_handler(program, handler: HandlerFn, *, env=None, store=None): return run( - WithHandler(handler, program), + handler(program), handlers=default_handlers(), env=env, store=store, @@ -65,7 +66,7 @@ async def _async_run_with_handler( program, handler: HandlerFn, *, env=None, store=None ): return await async_run( - WithHandler(handler, program), + handler(program), handlers=default_async_handlers(), env=env, store=store, @@ -208,7 +209,7 @@ def mock_preset_config(effect: Effect, k: Any): return (yield Resume(k, False)) yield Pass() - program = WithHandler(mock_preset_config, workflow()) + program = _install_raw_handler(mock_preset_config)(workflow()) result = _run_with_handler(program, preset_handlers()) assert result.value is False @@ -281,10 +282,7 @@ def workflow(): return custom_result # Stack handlers explicitly: inner custom handler shadows outer preset handler. - stacked_program = WithHandler( - preset_handlers(), - WithHandler(handle_custom, workflow()), - ) + stacked_program = preset_handlers()(_install_raw_handler(handle_custom)(workflow())) result = run(stacked_program, handlers=default_handlers()) assert result.value == "handled: test" diff --git a/packages/doeff-secret/src/doeff_secret/handlers.py b/packages/doeff-secret/src/doeff_secret/handlers.py index dc8bf97b7..146380ce8 100644 --- a/packages/doeff-secret/src/doeff_secret/handlers.py +++ b/packages/doeff-secret/src/doeff_secret/handlers.py @@ -6,6 +6,7 @@ from typing import Any from doeff import Delegate, Effect, Resume, do +from doeff import handler as _program_handler from .effects import GetSecret @@ -84,7 +85,7 @@ def handler(effect: Effect, k: Any): return (yield handle_get_secret(effect, k)) yield Delegate() - return handler + return _program_handler(handler) def env_var_handlers( @@ -116,7 +117,7 @@ def handler(effect: Effect, k: Any): raise KeyError(f"Secret not found in environment variables: {effect.secret_id}") return (yield Resume(k, value)) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-secret/src/doeff_secret/testing.py b/packages/doeff-secret/src/doeff_secret/testing.py index e4fc9f46a..ed7fc816f 100644 --- a/packages/doeff-secret/src/doeff_secret/testing.py +++ b/packages/doeff-secret/src/doeff_secret/testing.py @@ -6,6 +6,7 @@ from typing import Any, TypeAlias from doeff import Delegate, Effect, Resume, do +from doeff import handler as _program_handler from .effects import DeleteSecret, GetSecret, ListSecrets, SetSecret @@ -110,7 +111,7 @@ def handler(effect: Effect, k: Any): return (yield Resume(k, None)) yield Delegate() - return handler + return _program_handler(handler) def in_memory_handler( diff --git a/packages/doeff-secret/tests/test_effect_handlers.py b/packages/doeff-secret/tests/test_effect_handlers.py index d1d7083ce..a24c4549c 100644 --- a/packages/doeff-secret/tests/test_effect_handlers.py +++ b/packages/doeff-secret/tests/test_effect_handlers.py @@ -18,7 +18,7 @@ in_memory_handlers, ) -from doeff import WithHandler, default_handlers, do, run # noqa: E402 +from doeff import default_handlers, do, run # noqa: E402 def _is_ok(run_result: Any) -> bool: @@ -28,7 +28,7 @@ def _is_ok(run_result: Any) -> bool: def _run_with_handler(program, handler): return run( - WithHandler(handler, program), + handler(program), handlers=default_handlers(), ) @@ -69,13 +69,7 @@ def test_in_memory_handlers_support_secret_crud() -> None: def test_in_memory_handler_delegates_when_stacked() -> None: result = run( - WithHandler( - env_var_handler(environ={}), - WithHandler( - in_memory_handler(seed_data={"db-password": "from-memory"}), - _read_secret("db-password"), - ), - ), + env_var_handler(environ={})(in_memory_handler(seed_data={"db-password": "from-memory"})(_read_secret("db-password"))), handlers=default_handlers(), ) @@ -100,10 +94,7 @@ def test_env_var_handler_prefers_prefix_when_configured() -> None: } result = run( - WithHandler( - env_var_handler(environ=env, prefix="service"), - _read_secret("db-password"), - ), + env_var_handler(environ=env, prefix="service")(_read_secret("db-password")), handlers=default_handlers(), ) @@ -120,10 +111,7 @@ def test_env_var_handler_uses_raw_secret_id_when_enabled() -> None: env = {"db-password": "raw-name"} result = run( - WithHandler( - env_var_handler(environ=env, include_raw_secret_id=True), - _read_secret("db-password"), - ), + env_var_handler(environ=env, include_raw_secret_id=True)(_read_secret("db-password")), handlers=default_handlers(), ) @@ -135,7 +123,7 @@ def test_env_var_handler_can_read_process_environment(monkeypatch) -> None: monkeypatch.setenv("DB_PASSWORD", "from-process-env") result = run( - WithHandler(env_var_handler(environ=os.environ), _read_secret("db-password")), + env_var_handler(environ=os.environ)(_read_secret("db-password")), handlers=default_handlers(), ) diff --git a/packages/doeff-seedream/src/doeff_seedream/handlers/production.py b/packages/doeff-seedream/src/doeff_seedream/handlers/production.py index 102703f83..160d45c47 100644 --- a/packages/doeff-seedream/src/doeff_seedream/handlers/production.py +++ b/packages/doeff-seedream/src/doeff_seedream/handlers/production.py @@ -9,6 +9,7 @@ from doeff_image.types import ImageResult from doeff import EffectGenerator, Pass, Resume, do +from doeff import handler as _program_handler from doeff_seedream.effects import SeedreamGenerate, SeedreamStructuredOutput from doeff_seedream.structured_llm import _edit_image__seedream4_impl from doeff_seedream.types import SeedreamImageEditResult @@ -175,7 +176,7 @@ def handler(effect: Effect, k: Any): # noqa: PLR0911 return (yield Resume(k, value)) yield Pass() - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-seedream/src/doeff_seedream/handlers/testing.py b/packages/doeff-seedream/src/doeff_seedream/handlers/testing.py index 54c36208d..8648f3285 100644 --- a/packages/doeff-seedream/src/doeff_seedream/handlers/testing.py +++ b/packages/doeff-seedream/src/doeff_seedream/handlers/testing.py @@ -14,6 +14,7 @@ from PIL import Image as PILImage from doeff import Delegate, Resume, do +from doeff import handler as _program_handler from doeff_seedream.effects import SeedreamGenerate, SeedreamStructuredOutput from doeff_seedream.types import SeedreamImage, SeedreamImageEditResult @@ -205,7 +206,7 @@ def handler(effect: Effect, k: Any): return (yield Resume(k, active_handler.handle_structured(effect))) yield Delegate() - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-seedream/tests/test_edit_image.py b/packages/doeff-seedream/tests/test_edit_image.py index e6a4b3be5..bf105d411 100644 --- a/packages/doeff-seedream/tests/test_edit_image.py +++ b/packages/doeff-seedream/tests/test_edit_image.py @@ -1,5 +1,4 @@ # ruff: noqa: E402 - import base64 import sys from pathlib import Path @@ -8,6 +7,8 @@ import pytest from PIL import Image +from doeff import handler as _program_handler + IMAGE_PACKAGE_ROOT = Path(__file__).resolve().parents[2] / "doeff-image" / "src" if str(IMAGE_PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(IMAGE_PACKAGE_ROOT)) @@ -25,7 +26,6 @@ Pass, Resume, Try, - WithHandler, async_run, default_handlers, do, @@ -62,12 +62,12 @@ def _handler(effect: Effect, k: Any): return (yield Resume(k, overrides[effect.key])) yield Pass() - return _handler + return _program_handler(_handler) async def _run_with_handler(program, overrides: dict[str, Any]): return await async_run( - WithHandler(_build_mock_seedream_handler(overrides), program), + _build_mock_seedream_handler(overrides)(program), handlers=default_handlers(), ) diff --git a/packages/doeff-seedream/tests/test_effect_handlers.py b/packages/doeff-seedream/tests/test_effect_handlers.py index c518baa03..7de5333be 100644 --- a/packages/doeff-seedream/tests/test_effect_handlers.py +++ b/packages/doeff-seedream/tests/test_effect_handlers.py @@ -23,7 +23,7 @@ from doeff_seedream.handlers import mock_handlers, production_handlers from doeff_seedream.types import SeedreamImage, SeedreamImageEditResult -from doeff import EffectGenerator, WithHandler, default_handlers, do, run +from doeff import EffectGenerator, default_handlers, do, run class SummarySchema: @@ -63,7 +63,7 @@ def _build_result(*, prompt: str, model: str, payload: bytes) -> SeedreamImageEd def _run_with_handler(program, handler): return run( - WithHandler(handler, program), + handler(program), handlers=default_handlers(), ) diff --git a/packages/doeff-test-target/src/doeff_test_target/handlers/production.py b/packages/doeff-test-target/src/doeff_test_target/handlers/production.py index d7fb8bdf1..6e3e045ab 100644 --- a/packages/doeff-test-target/src/doeff_test_target/handlers/production.py +++ b/packages/doeff-test-target/src/doeff_test_target/handlers/production.py @@ -7,6 +7,7 @@ from doeff import Ask, Pass, Resume, Tell, do from doeff import EffectBase as Effect +from doeff import handler as _program_handler from doeff_test_target.effects import ReadFixtureValue, RecordFixtureEvent ProtocolHandler = Callable[[Any, Any], Any] @@ -53,7 +54,7 @@ def handler(effect: Effect, k: Any): return (yield from active_runtime.handle_record_event(effect, k)) return (yield Pass()) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-test-target/src/doeff_test_target/handlers/testing.py b/packages/doeff-test-target/src/doeff_test_target/handlers/testing.py index be1f36878..fb90e18a0 100644 --- a/packages/doeff-test-target/src/doeff_test_target/handlers/testing.py +++ b/packages/doeff-test-target/src/doeff_test_target/handlers/testing.py @@ -6,6 +6,7 @@ from typing import Any from doeff import Effect, Pass, Resume, do +from doeff import handler as _program_handler from doeff_test_target.effects import ReadFixtureValue, RecordFixtureEvent ProtocolHandler = Callable[[Any, Any], Any] @@ -51,7 +52,7 @@ def handler(effect: Effect, k: Any): return (yield from active_runtime.handle_record_event(effect, k)) return (yield Pass()) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-test-target/tests/test_effect_handlers.py b/packages/doeff-test-target/tests/test_effect_handlers.py index 68c37aecf..9a45d30cc 100644 --- a/packages/doeff-test-target/tests/test_effect_handlers.py +++ b/packages/doeff-test-target/tests/test_effect_handlers.py @@ -1,7 +1,7 @@ import sys from pathlib import Path -from doeff import WithHandler, default_handlers, run +from doeff import default_handlers, run sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) @@ -17,7 +17,7 @@ def _run_with_handler(program, handler, *, env=None): return run( - WithHandler(handler, program), + handler(program), handlers=default_handlers(), env=env, ) diff --git a/packages/doeff-test-target/tests/test_with_handler_patterns.py b/packages/doeff-test-target/tests/test_with_handler_patterns.py index 7bc126ed4..fc277fb13 100644 --- a/packages/doeff-test-target/tests/test_with_handler_patterns.py +++ b/packages/doeff-test-target/tests/test_with_handler_patterns.py @@ -4,7 +4,8 @@ import pytest -from doeff import Ask, AskEffect, Effect, Pass, Resume, WithHandler, default_handlers, do, run +from doeff import Ask, AskEffect, Effect, Pass, Resume, default_handlers, do, run +from doeff import handler as _install_raw_handler sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) @@ -41,7 +42,7 @@ def mock_handler(effect: Effect, k): return (yield Pass()) result = run( - WithHandler(mock_handler, _test_target_pipeline()), + _install_raw_handler(mock_handler)(_test_target_pipeline()), handlers=default_handlers(), ) @@ -62,7 +63,7 @@ def inner_handler(effect: Effect, k): return (yield Pass()) result = run( - WithHandler(outer_handler, WithHandler(inner_handler, _nested_ask_program())), + _install_raw_handler(outer_handler)(_install_raw_handler(inner_handler)(_nested_ask_program())), handlers=default_handlers(), ) @@ -80,7 +81,7 @@ def failing_handler(effect: Effect, _k): return (yield Pass()) result = run( - WithHandler(failing_handler, _single_ask_program("explode")), + _install_raw_handler(failing_handler)(_single_ask_program("explode")), handlers=default_handlers(), ) @@ -101,7 +102,7 @@ def delegating_handler(effect: Effect, _k): return (yield Pass()) result = run( - WithHandler(delegating_handler, _single_ask_program("service_name")), + _install_raw_handler(delegating_handler)(_single_ask_program("service_name")), handlers=default_handlers(), env={"service_name": "doeff-test-target"}, ) @@ -127,7 +128,7 @@ def typed_mock_handler(effect: Effect, k, sample_value=sample_value): return (yield Pass()) result = run( - WithHandler(typed_mock_handler, _single_ask_program("any-key")), + _install_raw_handler(typed_mock_handler)(_single_ask_program("any-key")), handlers=default_handlers(), ) diff --git a/packages/doeff-time/src/doeff_time/handlers/async_time.py b/packages/doeff-time/src/doeff_time/handlers/async_time.py index 4306cbae8..7ee9bb18c 100644 --- a/packages/doeff-time/src/doeff_time/handlers/async_time.py +++ b/packages/doeff-time/src/doeff_time/handlers/async_time.py @@ -10,6 +10,7 @@ from doeff_core_effects.scheduler import Spawn from doeff import Pass, Resume, do +from doeff import handler as _program_handler from doeff_time.effects import DelayEffect, GetTimeEffect, ScheduleAtEffect, WaitUntilEffect ProtocolHandler = Callable[[Any, Any], Any] @@ -86,7 +87,7 @@ def async_time_handler( def handler(effect: Any, k: Any): return (yield runtime.handle(effect, k)) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-time/src/doeff_time/handlers/sync_time.py b/packages/doeff-time/src/doeff_time/handlers/sync_time.py index dbe63b9cc..5dfbc42c1 100644 --- a/packages/doeff-time/src/doeff_time/handlers/sync_time.py +++ b/packages/doeff-time/src/doeff_time/handlers/sync_time.py @@ -11,6 +11,7 @@ from doeff_core_effects.scheduler import Wait as WaitTask from doeff import Pass, Resume, do +from doeff import handler as _program_handler from doeff_time.effects import DelayEffect, GetTimeEffect, ScheduleAtEffect, WaitUntilEffect ProtocolHandler = Callable[[Any, Any], Any] @@ -92,7 +93,7 @@ def sync_time_handler( def handler(effect: Any, k: Any): return (yield runtime.handle(effect, k)) - return handler + return _program_handler(handler) __all__ = [ diff --git a/packages/doeff-time/tests/conftest.py b/packages/doeff-time/tests/conftest.py index fb6e2b213..c46d5fbdc 100644 --- a/packages/doeff-time/tests/conftest.py +++ b/packages/doeff-time/tests/conftest.py @@ -3,6 +3,8 @@ from datetime import datetime, timedelta, timezone from pathlib import Path +from doeff import handler as _install_raw_handler + ROOT = Path(__file__).resolve().parents[3] TIME_PACKAGE_ROOT = ROOT / "packages" / "doeff-time" / "src" EVENTS_PACKAGE_ROOT = ROOT / "packages" / "doeff-events" / "src" @@ -30,7 +32,7 @@ def listen(program, types=None): """ from doeff_core_effects import WriterTellEffect - from doeff import Pass, WithHandler, do + from doeff import Pass, do types_to_collect = types or (WriterTellEffect,) collected = [] @@ -43,7 +45,7 @@ def collector(effect, k): @do def _listen(): - result = yield WithHandler(collector, program) + result = yield _install_raw_handler(collector)(program) return (result, collected) return _listen() @@ -60,17 +62,17 @@ def run_with_handlers(program, *, env=None): ) from doeff_core_effects.scheduler import scheduled - from doeff import WithHandler, run + from doeff import run wrapped = program if env is not None: - wrapped = WithHandler(reader(env=env), wrapped) + wrapped = reader(env=env)(wrapped) # await_handler uses scheduler effects (CreateExternalPromise, Wait), # so it must be inside the scheduler. - wrapped = WithHandler(await_handler(), wrapped) + wrapped = await_handler()(wrapped) wrapped = scheduled(wrapped) - wrapped = WithHandler(slog_handler(), wrapped) - wrapped = WithHandler(try_handler, wrapped) - wrapped = WithHandler(listen_handler, wrapped) - wrapped = WithHandler(writer(), wrapped) + wrapped = slog_handler()(wrapped) + wrapped = try_handler(wrapped) + wrapped = listen_handler(wrapped) + wrapped = writer()(wrapped) return run(wrapped) diff --git a/packages/doeff-time/tests/test_async_handler.py b/packages/doeff-time/tests/test_async_handler.py index f0a2b58cc..678e6e548 100644 --- a/packages/doeff-time/tests/test_async_handler.py +++ b/packages/doeff-time/tests/test_async_handler.py @@ -7,7 +7,7 @@ from doeff_time.handlers import async_time_handler from conftest import run_with_handlers -from doeff import WithHandler, do +from doeff import do @do @@ -28,7 +28,7 @@ def _delegate_probe_program(): def test_async_delay_uses_wall_clock_sleep() -> None: start = time.perf_counter() run_with_handlers( - WithHandler(async_time_handler(), _delay_program(0.03)), + async_time_handler()(_delay_program(0.03)), ) elapsed = time.perf_counter() - start @@ -39,7 +39,7 @@ def test_async_wait_until_blocks_until_target_time() -> None: target = datetime.now(timezone.utc) + timedelta(seconds=0.03) start = time.perf_counter() run_with_handlers( - WithHandler(async_time_handler(), _wait_until_program(target)), + async_time_handler()(_wait_until_program(target)), ) elapsed = time.perf_counter() - start @@ -48,7 +48,7 @@ def test_async_wait_until_blocks_until_target_time() -> None: def test_async_handler_delegates_non_time_effects() -> None: result = run_with_handlers( - WithHandler(async_time_handler(), _delegate_probe_program()), + async_time_handler()(_delegate_probe_program()), env={"delegated_key": "ok"}, ) assert result == "ok" diff --git a/packages/doeff-time/tests/test_get_time.py b/packages/doeff-time/tests/test_get_time.py index fcfbcff7c..7124879c2 100644 --- a/packages/doeff-time/tests/test_get_time.py +++ b/packages/doeff-time/tests/test_get_time.py @@ -5,7 +5,7 @@ from doeff_time.handlers import sync_time_handler from conftest import run_with_handlers -from doeff import WithHandler, do +from doeff import do @do @@ -16,7 +16,7 @@ def _read_time_program(): def test_get_time_sync_handler_returns_current_time() -> None: before = datetime.now(timezone.utc) result = run_with_handlers( - WithHandler(sync_time_handler(), _read_time_program()), + sync_time_handler()(_read_time_program()), ) after = datetime.now(timezone.utc) diff --git a/packages/doeff-time/tests/test_schedule_at.py b/packages/doeff-time/tests/test_schedule_at.py index 2a3f548a0..19dd0222a 100644 --- a/packages/doeff-time/tests/test_schedule_at.py +++ b/packages/doeff-time/tests/test_schedule_at.py @@ -7,7 +7,8 @@ from doeff_time.handlers import sim_time_handler from conftest import run_with_handlers -from doeff import Effect, EffectBase, Pass, Resume, WithHandler, do +from doeff import Effect, EffectBase, Pass, Resume, do +from doeff import handler as _program_handler def _make_marker_program(marker: dict[str, bool]): @@ -39,7 +40,7 @@ def _handler(effect: Effect, k): return (yield Resume(k, None)) yield Pass(effect, k) - return _handler + return _program_handler(_handler) def test_schedule_at_executes_program_when_clock_advances() -> None: @@ -55,7 +56,7 @@ def program(): yield Wait(task) run_with_handlers( - WithHandler(sim_time_handler(), program()), + sim_time_handler()(program()), ) assert marker["done"] is True @@ -72,9 +73,6 @@ def program(): yield Wait(task) run_with_handlers( - WithHandler( - _outer_handler(marker), - WithHandler(sim_time_handler(), program()), - ), + _outer_handler(marker)(sim_time_handler()(program())), ) assert marker["done"] is True diff --git a/packages/doeff-time/tests/test_sim_daemon_pattern.py b/packages/doeff-time/tests/test_sim_daemon_pattern.py index b2a8749b9..c9e779e7a 100644 --- a/packages/doeff-time/tests/test_sim_daemon_pattern.py +++ b/packages/doeff-time/tests/test_sim_daemon_pattern.py @@ -11,7 +11,7 @@ from doeff_time import Delay, GetTime, ScheduleAt, WaitUntil, sim_time_handler from conftest import SIM_TIME_EPOCH, run_with_handlers, sim_time -from doeff import WithHandler, do +from doeff import do def _run_sim( @@ -20,7 +20,7 @@ def _run_sim( start_time: datetime = SIM_TIME_EPOCH, ): return run_with_handlers( - WithHandler(sim_time_handler(start_time=start_time), program), + sim_time_handler(start_time=start_time)(program), ) @@ -30,12 +30,7 @@ def _run_sim_events( start_time: datetime = SIM_TIME_EPOCH, ): return run_with_handlers( - WithHandler(event_handler(), - WithHandler( - sim_time_handler(start_time=start_time), - program, - ), - ), + event_handler()(sim_time_handler(start_time=start_time)(program)), ) diff --git a/packages/doeff-time/tests/test_sim_migration_readiness.py b/packages/doeff-time/tests/test_sim_migration_readiness.py index 62200b63f..ab036b14f 100644 --- a/packages/doeff-time/tests/test_sim_migration_readiness.py +++ b/packages/doeff-time/tests/test_sim_migration_readiness.py @@ -32,7 +32,6 @@ from conftest import SIM_TIME_EPOCH, listen, run_with_handlers, sim_seconds, sim_time from doeff import ( - WithHandler, do, ) @@ -48,10 +47,7 @@ def _run_sim( log_formatter: Callable[[datetime, Any], str] | None = None, ): return run_with_handlers( - WithHandler( - sim_time_handler(start_time=start_time, log_formatter=log_formatter), - program, - ), + sim_time_handler(start_time=start_time, log_formatter=log_formatter)(program), ) @@ -62,12 +58,7 @@ def _run_sim_events( log_formatter: Callable[[datetime, Any], str] | None = None, ): return run_with_handlers( - WithHandler(event_handler(), - WithHandler( - sim_time_handler(start_time=start_time, log_formatter=log_formatter), - program, - ), - ), + event_handler()(sim_time_handler(start_time=start_time, log_formatter=log_formatter)(program)), ) @@ -650,13 +641,10 @@ def _program(): inner_result, collected = run_with_handlers( listen( - WithHandler( - sim_time_handler( + sim_time_handler( start_time=sim_time(100.0), log_formatter=lambda t, msg: f"[{sim_seconds(t):.0f}] {msg}", - ), - _program(), - ), + )(_program()), ), ) assert inner_result == "done" @@ -753,13 +741,10 @@ def _backtest(): inner_result, collected = run_with_handlers( listen( - WithHandler( - sim_time_handler( + sim_time_handler( start_time=sim_time(0.0), log_formatter=lambda t, msg: f"[t={sim_seconds(t):.0f}] {msg}", - ), - WithHandler(event_handler(), _backtest()), - ), + )(event_handler()(_backtest())), ), ) assert inner_result == "done" diff --git a/packages/doeff-time/tests/test_sim_time.py b/packages/doeff-time/tests/test_sim_time.py index 42aaae1b4..e25007586 100644 --- a/packages/doeff-time/tests/test_sim_time.py +++ b/packages/doeff-time/tests/test_sim_time.py @@ -15,9 +15,9 @@ from doeff import ( Effect, Pass, - WithHandler, do, ) +from doeff import handler as _install_raw_handler def _run_with_sim( @@ -27,10 +27,7 @@ def _run_with_sim( log_formatter: Callable[[datetime, Any], str] | None = None, ): return run_with_handlers( - WithHandler( - sim_time_handler(start_time=start_time, log_formatter=log_formatter), - program, - ), + sim_time_handler(start_time=start_time, log_formatter=log_formatter)(program), ) @@ -41,12 +38,7 @@ def _run_with_sim_and_events( log_formatter: Callable[[datetime, Any], str] | None = None, ): return run_with_handlers( - WithHandler(event_handler(), - WithHandler( - sim_time_handler(start_time=start_time, log_formatter=log_formatter), - program, - ), - ), + event_handler()(sim_time_handler(start_time=start_time, log_formatter=log_formatter)(program)), ) @@ -65,10 +57,7 @@ def probe(effect: Effect, k: Any): yield Pass(effect, k) result = run_with_handlers( - WithHandler( - probe, - WithHandler(sim_time_handler(start_time=sim_time(0.0)), program), - ), + _install_raw_handler(probe)(sim_time_handler(start_time=sim_time(0.0))(program)), ) return seen, result @@ -283,7 +272,7 @@ def _program(): # listen outside sim_time so it sees Tell from ScheduleAt's Spawn'd task inner_result, collected = run_with_handlers( - listen(WithHandler(sim_time_handler(start_time=sim_time(10.0)), _program())), + listen(sim_time_handler(start_time=sim_time(10.0))(_program())), ) assert inner_result == sim_time(15.0) assert [e.msg for e in collected] == ["scheduled"] @@ -298,7 +287,7 @@ def _program(): return (yield GetTime()) inner_result, collected = run_with_handlers( - listen(WithHandler(sim_time_handler(start_time=sim_time(0.0)), _program())), + listen(sim_time_handler(start_time=sim_time(0.0))(_program())), ) assert inner_result == sim_time(10.0) assert [e.msg for e in collected] == ["past"] @@ -356,13 +345,10 @@ def _program(): # listen must be OUTSIDE sim_time to see reformatted Tell messages inner_result, collected = run_with_handlers( listen( - WithHandler( - sim_time_handler( + sim_time_handler( start_time=sim_time(7.5), log_formatter=lambda current_time, msg: f"[sim:{sim_seconds(current_time):.1f}] {msg}", - ), - _program(), - ) + )(_program()) ), ) diff --git a/packages/doeff-time/tests/test_sync_handler.py b/packages/doeff-time/tests/test_sync_handler.py index 469fbc2f4..3d6685c29 100644 --- a/packages/doeff-time/tests/test_sync_handler.py +++ b/packages/doeff-time/tests/test_sync_handler.py @@ -7,7 +7,7 @@ from doeff_time.handlers import sync_time_handler from conftest import run_with_handlers -from doeff import WithHandler, do +from doeff import do @do @@ -28,7 +28,7 @@ def _delegate_probe_program(): def test_sync_delay_uses_wall_clock_sleep() -> None: start = time.perf_counter() run_with_handlers( - WithHandler(sync_time_handler(), _delay_program(0.03)), + sync_time_handler()(_delay_program(0.03)), ) elapsed = time.perf_counter() - start @@ -39,7 +39,7 @@ def test_sync_wait_until_blocks_until_target_time() -> None: target = datetime.now(timezone.utc) + timedelta(seconds=0.03) start = time.perf_counter() run_with_handlers( - WithHandler(sync_time_handler(), _wait_until_program(target)), + sync_time_handler()(_wait_until_program(target)), ) elapsed = time.perf_counter() - start @@ -48,7 +48,7 @@ def test_sync_wait_until_blocks_until_target_time() -> None: def test_sync_handler_delegates_non_time_effects() -> None: result = run_with_handlers( - WithHandler(sync_time_handler(), _delegate_probe_program()), + sync_time_handler()(_delegate_probe_program()), env={"delegated_key": "ok"}, ) assert result == "ok" diff --git a/packages/doeff-traverse/doeff_traverse/handlers.py b/packages/doeff-traverse/doeff_traverse/handlers.py index f55e3441a..e964c3771 100644 --- a/packages/doeff-traverse/doeff_traverse/handlers.py +++ b/packages/doeff-traverse/doeff_traverse/handlers.py @@ -7,6 +7,7 @@ from doeff import do from doeff.program import Pass, Resume, ResumeThrow +from doeff.program import handler as _program_handler from doeff_traverse.collection import Collection, HistoryEntry, ItemResult from doeff_traverse.effects import Fail, Inspect, Reduce, Skip, SortBy, Take, Traverse, Zip @@ -29,10 +30,6 @@ def sequential(): # noqa: PLR0915 - baseline cleanup keeps existing control flo from doeff_vm import Err, Ok from doeff.handler_utils import get_inner_handlers - from doeff.program import ( - WithHandler as WH, # noqa: N817 - existing local alias keeps handler code compact - ) - @do def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, Skip): @@ -61,14 +58,14 @@ def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup ke prog = effect.f(item.value) # Reinstall inner handlers + this handler for nested Traverse for h in inner_hs: - prog = WH(h, prog) - prog = WH(handler, prog) + prog = _program_handler(h)(prog) + prog = _program_handler(handler)(prog) # Wrap in Try to catch unhandled failures per item @do def attempt(prog=prog): from doeff_core_effects.handlers import try_handler - value = yield WH(try_handler, Try(prog)) + value = yield try_handler(Try(prog)) return value result = yield attempt() @@ -111,8 +108,8 @@ def attempt(prog=prog): for value in values_iter: prog = effect.f(acc, value) for h in inner_hs: - prog = WH(h, prog) - prog = WH(handler, prog) + prog = _program_handler(h)(prog) + prog = _program_handler(handler)(prog) acc = yield prog return (yield Resume(k, acc)) @@ -169,7 +166,7 @@ def attempt(prog=prog): yield Pass(effect, k) - return handler + return _program_handler(handler) def parallel(concurrency=10): # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged @@ -191,10 +188,6 @@ def parallel(concurrency=10): # noqa: PLR0915 - baseline cleanup keeps existing from doeff_vm import Err, Ok from doeff.handler_utils import get_inner_handlers - from doeff.program import ( - WithHandler as WH, # noqa: N817 - existing local alias keeps handler code compact - ) - @do def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, Skip): @@ -230,13 +223,13 @@ def run_item(item=item): yield AcquireSemaphore(sem) prog = effect.f(item.value) for h in inner_hs: - prog = WH(h, prog) - prog = WH(handler, prog) + prog = _program_handler(h)(prog) + prog = _program_handler(handler)(prog) @do def attempt(): from doeff_core_effects.handlers import try_handler - value = yield WH(try_handler, Try(prog)) + value = yield try_handler(Try(prog)) return value result = yield attempt() @@ -290,8 +283,8 @@ def attempt(): for value in values_iter: prog = effect.f(acc, value) for h in inner_hs: - prog = WH(h, prog) - prog = WH(handler, prog) + prog = _program_handler(h)(prog) + prog = _program_handler(handler)(prog) acc = yield prog return (yield Resume(k, acc)) @@ -346,7 +339,7 @@ def attempt(): yield Pass(effect, k) - return handler + return _program_handler(handler) def parallel_fail_fast(concurrency=10): # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged @@ -368,10 +361,6 @@ def parallel_fail_fast(concurrency=10): # noqa: PLR0915 - baseline cleanup keep ) from doeff.handler_utils import get_inner_handlers - from doeff.program import ( - WithHandler as WH, # noqa: N817 - existing local alias keeps handler code compact - ) - @do def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, Skip): @@ -409,8 +398,8 @@ def run_item(item=item): yield AcquireSemaphore(sem) prog = effect.f(item.value) for h in inner_hs: - prog = WH(h, prog) - prog = WH(handler, prog) + prog = _program_handler(h)(prog) + prog = _program_handler(handler)(prog) # NO Try wrapper — exceptions propagate to Gather result = yield prog yield ReleaseSemaphore(sem) @@ -451,8 +440,8 @@ def run_item(item=item): for value in values_iter: prog = effect.f(acc, value) for h in inner_hs: - prog = WH(h, prog) - prog = WH(handler, prog) + prog = _program_handler(h)(prog) + prog = _program_handler(handler)(prog) acc = yield prog return (yield Resume(k, acc)) @@ -507,11 +496,11 @@ def run_item(item=item): yield Pass(effect, k) - return handler + return _program_handler(handler) @do -def fail_handler(effect, k): +def _fail_handler(effect, k): """Default Fail handler: raises the cause as an exception. Converts unhandled Fail effects into Python exceptions. @@ -523,8 +512,13 @@ def fail_handler(effect, k): yield Pass(effect, k) +fail_handler = _program_handler(_fail_handler) +fail_handler.__name__ = "fail_handler" +fail_handler.__qualname__ = "fail_handler" + + @do -def normalize_to_none(effect, k): +def _normalize_to_none(effect, k): """Fail handler: resume with None at the fail site. The computation continues with None as the substitute value. @@ -532,3 +526,8 @@ def normalize_to_none(effect, k): if isinstance(effect, Fail): return (yield Resume(k, None)) yield Pass(effect, k) + + +normalize_to_none = _program_handler(_normalize_to_none) +normalize_to_none.__name__ = "normalize_to_none" +normalize_to_none.__qualname__ = "normalize_to_none" diff --git a/packages/doeff-traverse/examples/ex01_basic_fail.hy b/packages/doeff-traverse/examples/ex01_basic_fail.hy index 9c9f1c11a..368c8834a 100644 --- a/packages/doeff-traverse/examples/ex01_basic_fail.hy +++ b/packages/doeff-traverse/examples/ex01_basic_fail.hy @@ -6,7 +6,7 @@ (require doeff-hy.macros [defk <- do!]) (import doeff [do :as _doeff-do]) (import doeff [run]) -(import doeff.program [WithHandler Resume Pass]) +(import doeff.program [Resume Pass]) (import doeff_core_effects [try-handler :as try_handler]) (import doeff_core_effects.scheduler [scheduled]) @@ -41,14 +41,11 @@ (defn run-fail-fast [program] (run (scheduled - (WithHandler fail_handler - (WithHandler try_handler program))))) + (fail_handler (try_handler program))))) (defn run-normalize [program] (run (scheduled - (WithHandler fail_handler - (WithHandler normalize_to_none - (WithHandler try_handler program)))))) + (fail_handler (normalize_to_none (try_handler program)))))) (defk replace-with-zero [effect k] (if (isinstance effect Fail) @@ -57,9 +54,7 @@ (defn run-replace-zero [program] (run (scheduled - (WithHandler fail_handler - (WithHandler replace-with-zero - (WithHandler try_handler program)))))) + (fail_handler (replace-with-zero (try_handler program)))))) ;; --------------------------------------------------------------------------- diff --git a/packages/doeff-traverse/examples/ex02_traverse_pipeline.hy b/packages/doeff-traverse/examples/ex02_traverse_pipeline.hy index bc4737747..d0f7deea6 100644 --- a/packages/doeff-traverse/examples/ex02_traverse_pipeline.hy +++ b/packages/doeff-traverse/examples/ex02_traverse_pipeline.hy @@ -7,7 +7,7 @@ (require doeff-hy.macros [defk <- do!]) (import doeff [do :as _doeff-do]) (import doeff [run]) -(import doeff.program [WithHandler Resume Pass]) +(import doeff.program [Resume Pass]) (import doeff_core_effects [try-handler :as try_handler]) (import doeff_core_effects.scheduler [scheduled]) @@ -77,7 +77,7 @@ "Wrap program with a list of handlers (innermost first). Returns a program." (setv body program) (for [h stack] - (setv body (WithHandler h body))) + (setv body (h body))) (scheduled body)) diff --git a/packages/doeff-traverse/examples/ex03_zip_multi_source.hy b/packages/doeff-traverse/examples/ex03_zip_multi_source.hy index d48a12a1a..18c1607c3 100644 --- a/packages/doeff-traverse/examples/ex03_zip_multi_source.hy +++ b/packages/doeff-traverse/examples/ex03_zip_multi_source.hy @@ -7,7 +7,6 @@ (require doeff-hy.macros [defk <- do!]) (import doeff [do :as _doeff-do]) (import doeff [run]) -(import doeff.program [WithHandler]) (import doeff_core_effects [try-handler :as try_handler]) (import doeff_core_effects.scheduler [scheduled]) @@ -80,9 +79,7 @@ (defn run-it [program] (run (scheduled - (WithHandler (sequential) - (WithHandler fail_handler - (WithHandler try_handler program)))))) + ((sequential) (fail_handler (try_handler program)))))) (setv items ["hello world" "error_embed here" diff --git a/packages/doeff-traverse/examples/ex04_simple.hy b/packages/doeff-traverse/examples/ex04_simple.hy index 96fe6eb07..eb47d76b2 100644 --- a/packages/doeff-traverse/examples/ex04_simple.hy +++ b/packages/doeff-traverse/examples/ex04_simple.hy @@ -3,7 +3,6 @@ (require doeff-hy.macros [defk <- traverse]) (import doeff [do :as _doeff-do]) (import doeff [run]) -(import doeff.program [WithHandler]) (import doeff_core_effects [try-handler :as try_handler]) (import doeff_core_effects.scheduler [scheduled]) @@ -24,9 +23,9 @@ results) (setv program (pipeline [1 2 3])) -(setv body (WithHandler try_handler program)) -(setv body (WithHandler fail_handler body)) -(setv body (WithHandler (sequential) body)) +(setv body (try_handler program)) +(setv body (fail_handler body)) +(setv body ((sequential) body)) (setv body (scheduled body)) (setv result (run body)) diff --git a/packages/doeff-traverse/examples/ex04_traverse_macro.hy b/packages/doeff-traverse/examples/ex04_traverse_macro.hy index c14eb024c..b355167dc 100644 --- a/packages/doeff-traverse/examples/ex04_traverse_macro.hy +++ b/packages/doeff-traverse/examples/ex04_traverse_macro.hy @@ -21,7 +21,7 @@ (require doeff-hy.macros [defk <- traverse fnk fold]) (import doeff [do :as _doeff-do]) (import doeff [run]) -(import doeff.program [WithHandler Resume Pass]) +(import doeff.program [Resume Pass]) (import doeff_core_effects [try-handler :as try_handler]) (import doeff_core_effects.scheduler [scheduled]) @@ -123,7 +123,7 @@ "Wrap program with handlers (innermost first). Returns a runnable program." (setv body program) (for [h stack] - (setv body (WithHandler h body))) + (setv body (h body))) (scheduled body)) ;; base-stack: Fail → exception → per-item catch by sequential → item marked failed diff --git a/packages/doeff-traverse/examples/ex05_parallel_timing.hy b/packages/doeff-traverse/examples/ex05_parallel_timing.hy index 02e69374b..61215371c 100644 --- a/packages/doeff-traverse/examples/ex05_parallel_timing.hy +++ b/packages/doeff-traverse/examples/ex05_parallel_timing.hy @@ -11,7 +11,7 @@ (require doeff-hy.macros [defk <- traverse fnk]) (import doeff [do :as _doeff-do]) (import doeff [run EffectBase]) -(import doeff.program [WithHandler Resume Pass]) +(import doeff.program [Resume Pass]) (import doeff_core_effects [try-handler :as try_handler Await]) (import doeff_core_effects.handlers [await-handler :as await_handler]) @@ -94,7 +94,7 @@ async def _async_sleep_compute(x): (defn with-stack [stack program] (setv body program) (for [h stack] - (setv body (WithHandler h body))) + (setv body (h body))) (scheduled body)) (defn timed-run [label stack] diff --git a/packages/doeff-traverse/examples/ex06_mediagen_rewrite.hy b/packages/doeff-traverse/examples/ex06_mediagen_rewrite.hy index 561a7a338..06488695a 100644 --- a/packages/doeff-traverse/examples/ex06_mediagen_rewrite.hy +++ b/packages/doeff-traverse/examples/ex06_mediagen_rewrite.hy @@ -15,7 +15,7 @@ (require doeff-hy.macros [defk <- traverse fnk]) (import doeff [do :as _doeff-do]) (import doeff [run EffectBase]) -(import doeff.program [WithHandler Resume Pass]) +(import doeff.program [Resume Pass]) (import doeff_core_effects [try-handler :as try_handler Ask slog]) (import doeff_core_effects.handlers [lazy-ask :as lazy_ask]) @@ -172,7 +172,7 @@ (defn with-stack [stack program] (setv body program) (for [h stack] - (setv body (WithHandler h body))) + (setv body (h body))) (scheduled body)) (setv env {"s1a_model" "gpt-4o" diff --git a/packages/doeff-traverse/examples/ex07_proboscis_ema_rewrite.hy b/packages/doeff-traverse/examples/ex07_proboscis_ema_rewrite.hy index 8931cb1b4..1f4dcaf07 100644 --- a/packages/doeff-traverse/examples/ex07_proboscis_ema_rewrite.hy +++ b/packages/doeff-traverse/examples/ex07_proboscis_ema_rewrite.hy @@ -19,7 +19,7 @@ (require doeff-hy.macros [defk <- traverse fold]) (import doeff [do :as _doeff-do]) (import doeff [run EffectBase]) -(import doeff.program [WithHandler Resume Pass]) +(import doeff.program [Resume Pass]) (import doeff_core_effects [try-handler :as try_handler]) (import doeff_core_effects.scheduler [scheduled]) @@ -175,7 +175,7 @@ (defn with-stack [stack program] (setv body program) (for [h stack] - (setv body (WithHandler h body))) + (setv body (h body))) (scheduled body)) (setv events ["AAPL" "BADCO" "GOOGL" "CLOSED" "MSFT"]) diff --git a/packages/doeff-traverse/examples/ex08_tqdm_progress.hy b/packages/doeff-traverse/examples/ex08_tqdm_progress.hy index ba96b8c93..4ab77e530 100644 --- a/packages/doeff-traverse/examples/ex08_tqdm_progress.hy +++ b/packages/doeff-traverse/examples/ex08_tqdm_progress.hy @@ -9,7 +9,7 @@ (require doeff-hy.macros [defk <- traverse fold]) (import doeff [do :as _doeff-do]) (import doeff [run EffectBase]) -(import doeff.program [WithHandler Resume Pass]) +(import doeff.program [Resume Pass]) (import doeff_core_effects [try-handler :as try_handler Await]) (import doeff_core_effects.handlers [await-handler :as await_handler]) @@ -114,7 +114,7 @@ async def async_compute(x): (defn with-stack [stack program] (setv body program) (for [h stack] - (setv body (WithHandler h body))) + (setv body (h body))) (scheduled body)) (setv items (list (range 1 21))) diff --git a/packages/doeff-traverse/examples/ex09_for_do_when.hy b/packages/doeff-traverse/examples/ex09_for_do_when.hy index 72616f9f9..953b10ddd 100644 --- a/packages/doeff-traverse/examples/ex09_for_do_when.hy +++ b/packages/doeff-traverse/examples/ex09_for_do_when.hy @@ -12,7 +12,6 @@ (require doeff-hy.macros [defk <- for/do fold]) (import doeff [do :as _doeff-do]) (import doeff [run]) -(import doeff.program [WithHandler]) (import doeff_core_effects [try-handler :as try_handler]) (import doeff_core_effects.scheduler [scheduled]) @@ -88,7 +87,7 @@ (defn with-stack [stack program] (setv body program) (for [h stack] - (setv body (WithHandler h body))) + (setv body (h body))) (scheduled body)) (setv stack [try_handler fail_handler (sequential)]) diff --git a/packages/doeff-traverse/tests/test_memory_leak_multi_day.py b/packages/doeff-traverse/tests/test_memory_leak_multi_day.py index c2512ce9f..71a43aa88 100644 --- a/packages/doeff-traverse/tests/test_memory_leak_multi_day.py +++ b/packages/doeff-traverse/tests/test_memory_leak_multi_day.py @@ -35,6 +35,7 @@ from doeff_vm import WithHandler from doeff import EffectBase, Pass, Program, Resume, do, run, slog +from doeff import handler as _program_handler SIM_EPOCH = datetime(2025, 1, 1, tzinfo=timezone.utc) @@ -63,7 +64,7 @@ def _handler(effect, k): return (yield Resume(k, True)) yield Pass(effect, k) - return _handler + return _program_handler(_handler) @do diff --git a/packages/doeff-traverse/tests/test_traverse_deep_recursion.py b/packages/doeff-traverse/tests/test_traverse_deep_recursion.py index 0300814d4..3e363ce89 100644 --- a/packages/doeff-traverse/tests/test_traverse_deep_recursion.py +++ b/packages/doeff-traverse/tests/test_traverse_deep_recursion.py @@ -33,6 +33,7 @@ from doeff_vm import WithHandler from doeff import EffectBase, Pass, Program, Resume, do, run, slog +from doeff import handler as _program_handler @do @@ -305,7 +306,7 @@ def _make_passthrough_handler(tag: str): def _handler(effect, k): yield Pass(effect, k) - return _handler + return _program_handler(_handler) def _run_deep_stack(program, n_extra_handlers=15, concurrency=40): @@ -456,7 +457,7 @@ def _handler(effect, k): return (yield Resume(k, result)) yield Pass(effect, k) - return _handler + return _program_handler(_handler) def _run_deep_catching_stack(program, concurrency=40): diff --git a/packages/doeff-vm/tests/test_memory_stats.py b/packages/doeff-vm/tests/test_memory_stats.py index fa43e9e59..f600b8247 100644 --- a/packages/doeff-vm/tests/test_memory_stats.py +++ b/packages/doeff-vm/tests/test_memory_stats.py @@ -18,6 +18,7 @@ def handler(effect, k): return (yield doeff_vm.Resume(k, effect.key)) yield doeff_vm.Pass(effect, k) + # Direct VM node test: doeff_vm.WithHandler expects the raw dispatcher. return handler diff --git a/tests/_run_helpers.py b/tests/_run_helpers.py index 199ecb929..ba5116dae 100644 --- a/tests/_run_helpers.py +++ b/tests/_run_helpers.py @@ -26,7 +26,8 @@ ) from doeff_core_effects.scheduler import scheduled -from doeff import Err, Ok, WithHandler +from doeff import Err, Ok +from doeff import handler as _program_handler from doeff import run as _run @@ -55,7 +56,7 @@ def wrap_with_defaults(program: Any, env: Any = None, store: Any = None) -> Any: """Wrap ``program`` with the default handler chain + scheduler.""" wrapped = program for handler in reversed(default_handlers(env=env, store=store)): - wrapped = WithHandler(handler, wrapped) + wrapped = _program_handler(handler)(wrapped) return scheduled(wrapped) diff --git a/tests/architecture/test_no_public_withhandler_shim.py b/tests/architecture/test_no_public_withhandler_shim.py new file mode 100644 index 000000000..371c085dd --- /dev/null +++ b/tests/architecture/test_no_public_withhandler_shim.py @@ -0,0 +1,51 @@ +"""Guard against reintroducing the public WithHandler compatibility shim.""" + +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +BANNED_IMPORTS = ( + re.compile(r"^\s*from\s+doeff\s+import\s+.*\bWithHandler\b", re.MULTILINE), + re.compile(r"^\s*from\s+doeff\.program\s+import\s+.*\bWithHandler\b", re.MULTILINE), + re.compile(r"\(import\s+doeff\s+\[[^\]]*\bWithHandler\b", re.MULTILINE), + re.compile(r"\(import\s+doeff\.program\s+\[[^\]]*\bWithHandler\b", re.MULTILINE), +) +BANNED_CALLS = ( + re.compile(r"\bdoeff\.WithHandler\s*\("), + re.compile(r"\bprogram\.WithHandler\s*\("), +) + + +def _source_files() -> list[Path]: + roots = ("doeff", "packages", "tests", "examples") + return [ + path + for root in roots + for path in (REPO_ROOT / root).rglob("*") + if path.suffix in {".py", ".pyi", ".hy"} + and path != Path(__file__).resolve() + and ".venv" not in path.parts + and "semgrep/fixtures" not in path.as_posix() + ] + + +def test_public_modules_do_not_export_withhandler() -> None: + import doeff + import doeff.program + + assert not hasattr(doeff, "WithHandler") + assert not hasattr(doeff.program, "WithHandler") + + +def test_public_withhandler_shim_imports_are_gone() -> None: + offenders: list[str] = [] + for path in _source_files(): + text = path.read_text(encoding="utf-8") + for pattern in (*BANNED_IMPORTS, *BANNED_CALLS): + if pattern.search(text): + offenders.append(str(path.relative_to(REPO_ROOT))) + break + + assert offenders == [] diff --git a/tests/cli/test_cli_error_traces.py b/tests/cli/test_cli_error_traces.py index d9212d423..5105c9dcd 100644 --- a/tests/cli/test_cli_error_traces.py +++ b/tests/cli/test_cli_error_traces.py @@ -118,24 +118,24 @@ def test_handler_error_shows_handler_context() -> None: "-c", """ from dataclasses import dataclass -from doeff import Delegate, Effect, EffectBase, Program, WithHandler, do +from doeff import Effect, EffectBase, Pass, Program, do, handler @dataclass(frozen=True, kw_only=True) class Boom(EffectBase): pass @do -def bad_handler(effect: Effect, _k): +def bad_handler(effect: Effect, k): if isinstance(effect, Boom): raise RuntimeError("handler exploded") - yield Delegate() + yield Pass(effect, k) @do def body() -> Program[int]: yield Boom() return 1 -WithHandler(bad_handler, body()) +handler(bad_handler)(body()) """, "--interpreter", "tests.cli.cli_assets.sync_interpreter", diff --git a/tests/cli/test_cli_main.py b/tests/cli/test_cli_main.py index 24d04d045..fb9719a56 100644 --- a/tests/cli/test_cli_main.py +++ b/tests/cli/test_cli_main.py @@ -7,6 +7,7 @@ import pytest from doeff import __main__ as cli +from doeff import handler as _program_handler from doeff import run pytestmark = pytest.mark.cli @@ -70,7 +71,6 @@ def _env_interpreter(program, env=None): ) from doeff_core_effects.scheduler import scheduled - from doeff import WithHandler env_dict = {} if env is not None: @@ -83,7 +83,7 @@ def _env_interpreter(program, env=None): ] wrapped = program for h in reversed(handlers): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) return run(scheduled(wrapped)) diff --git a/tests/conftest.py b/tests/conftest.py index 059b3c6f0..0fbfe17b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,8 @@ ) from doeff_core_effects.scheduler import scheduled -from doeff import WithHandler, run +from doeff import handler as _program_handler +from doeff import run T = TypeVar("T") @@ -72,7 +73,7 @@ def run( handlers = default_handlers(env=env) wrapped = program for h in reversed(handlers): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) # Wrap with scheduler (outermost effect handler) wrapped = scheduled(wrapped) return run(wrapped) diff --git a/tests/core/test_delegate_non_terminal.py b/tests/core/test_delegate_non_terminal.py index cb72875c9..3ae358368 100644 --- a/tests/core/test_delegate_non_terminal.py +++ b/tests/core/test_delegate_non_terminal.py @@ -7,9 +7,9 @@ EffectBase, Pass, Resume, - WithHandler, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults @@ -38,7 +38,7 @@ def body(): _ = yield _ProbeEffect() return -1 # unreachable (inner handler does not resume k_user) - result = run_with_defaults(WithHandler(outer_handler, WithHandler(inner_handler, body()))) + result = run_with_defaults(_install_raw_handler(outer_handler)(_install_raw_handler(inner_handler)(body()))) assert result.value == 42 assert observed["raw"] == 42 @@ -62,7 +62,7 @@ def body(): x = yield _ProbeEffect() return x + 1 - result = run_with_defaults(WithHandler(outer_handler, WithHandler(inner_handler, body()))) + result = run_with_defaults(_install_raw_handler(outer_handler)(_install_raw_handler(inner_handler)(body()))) assert result.value == 43 @@ -91,10 +91,7 @@ def handler_c(effect: Effect, k): def body(): return (yield _ProbeEffect()) - result = run_with_defaults(WithHandler( - handler_c, - WithHandler(handler_b, WithHandler(handler_a, body())), - )) + result = run_with_defaults(_install_raw_handler(handler_c)(_install_raw_handler(handler_b)(_install_raw_handler(handler_a)(body())))) assert result.value == "c-b-a" @@ -123,10 +120,7 @@ def handler_c(effect: Effect, k): def body(): return (yield _ProbeEffect()) - result = run_with_defaults(WithHandler( - handler_c, - WithHandler(handler_b, WithHandler(handler_a, body())), - )) + result = run_with_defaults(_install_raw_handler(handler_c)(_install_raw_handler(handler_b)(_install_raw_handler(handler_a)(body())))) assert result.value == 42 @@ -152,7 +146,7 @@ def body(): body_resumed["value"] = True return "user-path" - result = run_with_defaults(WithHandler(outer_handler, WithHandler(inner_handler, body()))) + result = run_with_defaults(_install_raw_handler(outer_handler)(_install_raw_handler(inner_handler)(body()))) assert result.value == "inner:outer" assert body_resumed["value"] is False @@ -176,5 +170,5 @@ def program(): x = yield Ask("key") return x + 1 - result = run_with_defaults(WithHandler(outer_handler, WithHandler(inner_handler, program()))) + result = run_with_defaults(_install_raw_handler(outer_handler)(_install_raw_handler(inner_handler)(program()))) assert result.value == 85 diff --git a/tests/core/test_get_execution_context_effect.py b/tests/core/test_get_execution_context_effect.py index 1254cddab..1cddbc3d9 100644 --- a/tests/core/test_get_execution_context_effect.py +++ b/tests/core/test_get_execution_context_effect.py @@ -8,7 +8,6 @@ GetExecutionContext, Pass, Resume, - WithHandler, ) # REMOVED: from doeff import ProgramCallStack @@ -18,6 +17,7 @@ Program, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults # REMOVED: from doeff.traceback import build_doeff_traceback @@ -62,7 +62,7 @@ def observer(effect: Effect, k: object): def failing_program() -> Program[None]: raise KeyboardInterrupt("stop") - wrapped = WithHandler(observer, failing_program()) + wrapped = _install_raw_handler(observer)(failing_program()) result = run_with_defaults(wrapped) assert result.is_err() assert isinstance(result.error, KeyboardInterrupt) diff --git a/tests/core/test_kleisli_characterization.py b/tests/core/test_kleisli_characterization.py index e04785324..76e23f263 100644 --- a/tests/core/test_kleisli_characterization.py +++ b/tests/core/test_kleisli_characterization.py @@ -12,9 +12,9 @@ Put, Resume, Tell, - WithHandler, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults @@ -51,7 +51,7 @@ def body(): return value def main(): - return (yield WithHandler(handler, _prog(body))) + return (yield _install_raw_handler(handler)(_prog(body))) result = run_with_defaults(_prog(main)) assert result.value == "pong:hello" @@ -69,7 +69,7 @@ def body(): return value def main(): - return (yield WithHandler(handler, _prog(body))) + return (yield _install_raw_handler(handler)(_prog(body))) result = run_with_defaults(_prog(main), store={"key": "magic"}) assert result.value == "got:magic" @@ -89,7 +89,7 @@ def body(): return f"{first}|{second}" def main(): - return (yield WithHandler(handler, _prog(body))) + return (yield _install_raw_handler(handler)(_prog(body))) result = run_with_defaults(_prog(main)) assert result.value == "ping:a|pong:b" @@ -147,7 +147,7 @@ def body(): return f"{from_handler}|{current}" result = run_with_defaults( - WithHandler(handler, body()), + _install_raw_handler(handler)(body()), store={"counter": 10}, ) assert result.value == "counter:10|11" @@ -167,7 +167,7 @@ def body(): return value def main(): - return (yield WithHandler(handler, _prog(body))) + return (yield _install_raw_handler(handler)(_prog(body))) result = run_with_defaults(_prog(main)) assert result.value == "handled:hello" @@ -186,7 +186,7 @@ def body(): return value def main(): - return (yield WithHandler(handler, _prog(body))) + return (yield _install_raw_handler(handler)(_prog(body))) result = run_with_defaults(_prog(main), store={"key": "magic"}) assert result.value == "handled:magic" @@ -212,7 +212,7 @@ def body(): return value result = run_with_defaults( - WithHandler(outer_handler, WithHandler(inner_handler, _prog(body))), + _install_raw_handler(outer_handler)(_install_raw_handler(inner_handler)(_prog(body))), ) assert result.value == "inner:outer:from-inner-body" @@ -235,7 +235,7 @@ def body(): return (yield Ping("x")) result = run_with_defaults( - WithHandler(outer_handler, WithHandler(inner_handler, _prog(body))), + _install_raw_handler(outer_handler)(_install_raw_handler(inner_handler)(_prog(body))), ) assert result.value == "inner" @@ -252,7 +252,7 @@ def handler(effect: Effect, k): def body(): return (yield Ping("x")) - result = run_with_defaults(WithHandler(handler, body())) + result = run_with_defaults(_install_raw_handler(handler)(body())) assert result.value == "plain:x" @@ -268,7 +268,7 @@ def handler(effect: Effect, k): def body(): return (yield Ping("y")) - result = run_with_defaults(WithHandler(handler, body())) + result = run_with_defaults(_install_raw_handler(handler)(body())) assert result.value == "do:y" @@ -286,7 +286,7 @@ def body(): @do def combined(): - value = yield WithHandler(handler, body()) + value = yield _install_raw_handler(handler)(body()) return f"ret:{value}" result = run_with_defaults(combined()) diff --git a/tests/core/test_nested_do_spawn_gather.py b/tests/core/test_nested_do_spawn_gather.py index 47147223b..4481deab2 100644 --- a/tests/core/test_nested_do_spawn_gather.py +++ b/tests/core/test_nested_do_spawn_gather.py @@ -28,10 +28,10 @@ Resume, Spawn, Try, - WithHandler, do, slog, ) +from doeff import handler as _program_handler from tests._run_helpers import run_with_defaults ProgramLike = Any # removed shim @@ -66,7 +66,7 @@ def _handler(effect: Effect, k): svc = yield Ask("service") data = yield svc.fetch(effect.key) return (yield Resume(k, data)) - return _handler + return _program_handler(_handler) # --- Pipeline pieces --- @@ -138,7 +138,7 @@ def test() -> EffectGenerator[list]: programs = [wrap_sem(Try(fetch_one(f"k{i}")), sem) for i in range(3)] return (yield my_async_gather(*programs)) - wrapped = WithHandler(make_handler(), test()) + wrapped = make_handler()(test()) wrapped = Local(_env(), wrapped) r = run_with_defaults(wrapped) assert r.is_ok(), f"Failed: {r.error}" @@ -152,7 +152,7 @@ def test() -> EffectGenerator[list]: programs = [Try(fetch_one(f"k{i}")) for i in range(3)] return (yield throttled_gather(*programs, concurrency=2)) - wrapped = WithHandler(make_handler(), test()) + wrapped = make_handler()(test()) wrapped = Local(_env(), wrapped) r = run_with_defaults(wrapped) assert r.is_ok(), f"Failed: {r.error}" @@ -168,7 +168,7 @@ def test() -> EffectGenerator[list]: *programs, concurrency=2, description="testing" )) - wrapped = WithHandler(make_handler(), test()) + wrapped = make_handler()(test()) wrapped = Local(_env(), wrapped) r = run_with_defaults(wrapped) assert r.is_ok(), f"Failed: {r.error}" diff --git a/tests/core/test_pass_primitive.py b/tests/core/test_pass_primitive.py index 282e6bedc..689cc5bc9 100644 --- a/tests/core/test_pass_primitive.py +++ b/tests/core/test_pass_primitive.py @@ -2,7 +2,8 @@ import pytest -from doeff import Effect, EffectBase, Pass, Resume, WithHandler, do +from doeff import Effect, EffectBase, Pass, Resume, do +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults @@ -37,7 +38,7 @@ def body(): return value result = run_with_defaults( - WithHandler(outer_handler, WithHandler(inner_handler, body())), + _install_raw_handler(outer_handler)(_install_raw_handler(inner_handler)(body())), ) assert result.value == "handled-by-outer" assert resumed_after_pass["value"] is False diff --git a/tests/core/test_runtime_regressions_manual.py b/tests/core/test_runtime_regressions_manual.py index 3e4e73f41..0511692c4 100644 --- a/tests/core/test_runtime_regressions_manual.py +++ b/tests/core/test_runtime_regressions_manual.py @@ -16,9 +16,9 @@ Resume, Spawn, Try, - WithHandler, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults @@ -84,7 +84,7 @@ def inner(): @do def program(): - return (yield Try(WithHandler(handler, inner()))) + return (yield Try(_install_raw_handler(handler)(inner()))) result = run_with_defaults(program()) assert result.is_ok() diff --git a/tests/core/test_spawn_gather_with_intercept.py b/tests/core/test_spawn_gather_with_intercept.py index ec7c280ff..f615fe557 100644 --- a/tests/core/test_spawn_gather_with_intercept.py +++ b/tests/core/test_spawn_gather_with_intercept.py @@ -37,9 +37,9 @@ Resume, Spawn, Try, - WithHandler, do, ) +from doeff import handler as _program_handler from tests._run_helpers import run_with_defaults # --- Custom effect (simulates HistoricalPriceEffect) --- @@ -113,7 +113,7 @@ def _handler(effect: Effect, k): data = yield svc.fetch(effect.ticker, effect.start, effect.end) return (yield Resume(k, data)) - return _handler + return _program_handler(_handler) # --- Pipeline: nested Try + Spawn+Gather --- @@ -168,7 +168,7 @@ def fetch_movements( def compose_handlers(program, *handlers): result = program for handler in reversed(handlers): - result = WithHandler(handler, result) + result = _program_handler(handler)(result) return result diff --git a/tests/core/test_spawn_try_gather.py b/tests/core/test_spawn_try_gather.py index 875b9b123..e67263777 100644 --- a/tests/core/test_spawn_try_gather.py +++ b/tests/core/test_spawn_try_gather.py @@ -25,9 +25,9 @@ Resume, Spawn, Try, - WithHandler, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults ProgramLike = Any # removed API shim @@ -111,7 +111,7 @@ def test_program() -> EffectGenerator[list]: programs = [Try(compute(f"k{i}")) for i in range(3)] return (yield throttled_gather(*programs, concurrency=2)) - wrapped = WithHandler(handler, test_program()) + wrapped = _install_raw_handler(handler)(test_program()) result = run_with_defaults(wrapped) assert result.is_ok(), f"Failed: {result.error}" assert len(result.value) == 3 @@ -125,7 +125,7 @@ def test_program() -> EffectGenerator[list]: programs = [compute(f"k{i}") for i in range(3)] return (yield throttled_gather(*programs, concurrency=2)) - wrapped = WithHandler(handler, test_program()) + wrapped = _install_raw_handler(handler)(test_program()) result = run_with_defaults(wrapped) assert result.is_ok(), f"Failed: {result.error}" assert len(result.value) == 3 @@ -142,7 +142,7 @@ def test_program() -> EffectGenerator[list]: results.append(r) return results - wrapped = WithHandler(handler, test_program()) + wrapped = _install_raw_handler(handler)(test_program()) result = run_with_defaults(wrapped) assert result.is_ok(), f"Failed: {result.error}" assert len(result.value) == 3 diff --git a/tests/core/test_spec_gaps.py b/tests/core/test_spec_gaps.py index 4037c3363..e138aba9e 100644 --- a/tests/core/test_spec_gaps.py +++ b/tests/core/test_spec_gaps.py @@ -37,8 +37,8 @@ def _prog(gen_factory): # --------------------------------------------------------------------------- -# G8: Import paths for Resume/Delegate/Transfer/WithHandler/K -# Spec (SPEC-009 §8): from doeff import Resume, Delegate, Transfer, WithHandler, K +# G8: Import paths for Resume/Delegate/Transfer/K +# Spec (SPEC-009 §8): from doeff import Resume, Delegate, Transfer, K # Current: These are NOT re-exported from doeff/__init__.py — ImportError # --------------------------------------------------------------------------- @@ -46,11 +46,11 @@ def _prog(gen_factory): class TestG8ImportPaths: """G8: doeff top-level must re-export VM dispatch primitives.""" - def test_import_with_handler(self) -> None: - """from doeff import WithHandler must succeed.""" - from doeff import WithHandler + def test_with_handler_public_shim_removed(self) -> None: + """from doeff import WithHandler must stay removed.""" + import doeff - assert WithHandler is not None + assert not hasattr(doeff, "WithHandler") def test_import_resume(self) -> None: """from doeff import Resume must succeed.""" diff --git a/tests/core/test_unified_traceback_system.py b/tests/core/test_unified_traceback_system.py index 42b9872fe..cf6fb300c 100644 --- a/tests/core/test_unified_traceback_system.py +++ b/tests/core/test_unified_traceback_system.py @@ -2,10 +2,11 @@ from dataclasses import dataclass -from doeff_vm import Pass, Resume, WithHandler +from doeff_vm import Pass, Resume # REMOVED: from doeff import ProgramCallStack from doeff import Effect, EffectBase, Program, do +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults # REMOVED: from doeff.trace import TraceDispatch @@ -45,7 +46,7 @@ def body() -> Program[int]: result = yield NeedsHandler(value=7) return result - wrapped = WithHandler(outer_handler, WithHandler(inner_handler, body())) + wrapped = _install_raw_handler(outer_handler)(_install_raw_handler(inner_handler)(body())) result = run_with_defaults(wrapped) assert result.is_ok(), result.error assert result.value == 7 diff --git a/tests/core/test_vm_architecture_ocaml5.py b/tests/core/test_vm_architecture_ocaml5.py index 790dac3f2..25ba34346 100644 --- a/tests/core/test_vm_architecture_ocaml5.py +++ b/tests/core/test_vm_architecture_ocaml5.py @@ -17,9 +17,9 @@ Pass, Resume, Spawn, - WithHandler, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults # --------------------------------------------------------------------------- @@ -64,7 +64,7 @@ class MyEffect(EffectBase): val = yield MyEffect() return val - result = run_with_defaults(WithHandler(_handler, _program())) + result = run_with_defaults(_install_raw_handler(_handler)(_program())) assert result.is_ok() assert result.value == "resumed_value" @@ -89,7 +89,7 @@ class MyEffect(EffectBase): return (yield MyEffect()) result = run_with_defaults( - WithHandler(_double_resume_handler, _program()), + _install_raw_handler(_double_resume_handler)(_program()), ) assert result.is_err() assert "one-shot" in str(result.error).lower() or "consumed" in str( @@ -153,7 +153,7 @@ def _program(): return result result = run_with_defaults( - WithHandler(_shared_handler, _program()) + _install_raw_handler(_shared_handler)(_program()) ) assert result.is_ok() assert result.value == [42], f"Spawned task should see Put(42), got {result.value}" @@ -192,7 +192,7 @@ def _program(): return results result = run_with_defaults( - WithHandler(_counting_handler, _program()) + _install_raw_handler(_counting_handler)(_program()) ) assert result.is_ok() # All 10 tasks should have called the SAME handler instance @@ -236,7 +236,7 @@ def _program(): return list((yield Gather(*tasks))) result = run_with_defaults( - WithHandler(_handler, _program()) + _install_raw_handler(_handler)(_program()) ) assert result.is_ok() assert len(result.value) == 5 @@ -282,7 +282,7 @@ def _program(): return (yield GetCount()) result = run_with_defaults( - WithHandler(_counter_handler, _program()) + _install_raw_handler(_counter_handler)(_program()) ) assert result.is_ok() assert result.value == 3 diff --git a/tests/core/test_vm_legacy_tag_parser_fail_loud.py b/tests/core/test_vm_legacy_tag_parser_fail_loud.py index 82021bd69..768bcf40b 100644 --- a/tests/core/test_vm_legacy_tag_parser_fail_loud.py +++ b/tests/core/test_vm_legacy_tag_parser_fail_loud.py @@ -1,6 +1,6 @@ +import doeff_vm import pytest -import doeff_vm from doeff import do diff --git a/tests/effects/http_request_deftest_cases.hy b/tests/effects/http_request_deftest_cases.hy index 63b8dbe9e..7c2c6d0a5 100644 --- a/tests/effects/http_request_deftest_cases.hy +++ b/tests/effects/http_request_deftest_cases.hy @@ -3,7 +3,6 @@ (import pathlib [Path]) (import pytest) -(import doeff [WithHandler]) (import doeff_core_effects [HttpError HttpRequest HttpResponse]) (import doeff_core_effects.handlers [await-handler slog-handler]) (import doeff_core_effects.http_handlers [http-production-handler http-fixture-handler]) @@ -67,8 +66,7 @@ "https://example.test/final" 0.2)])) (setv logs (slog-handler)) (<- response - (WithHandler logs - (WithHandler (await-handler) + (logs ((await-handler) ((http-production-handler :client-factory (fn [] client) :sleep noop-sleep) (do! @@ -104,8 +102,7 @@ [(make-response 201 {"Content-Type" "application/json"} b"{}" "{}" "https://example.test/api" 0.1)])) (<- response - (WithHandler (slog-handler) - (WithHandler (await-handler) + ((slog-handler) ((await-handler) ((http-production-handler :client-factory (fn [] client) :sleep noop-sleep) (do! @@ -125,8 +122,7 @@ [(make-response 302 {"Location" "/next"} b"" "" "https://example.test/start" 0.1)])) (<- response - (WithHandler (slog-handler) - (WithHandler (await-handler) + ((slog-handler) ((await-handler) ((http-production-handler :client-factory (fn [] client) :sleep noop-sleep) (do! @@ -147,8 +143,7 @@ (make-response 200 {} b"ok" "ok" "https://example.test/api" 0.1)])) (setv sleeps []) (<- response - (WithHandler (slog-handler) - (WithHandler (await-handler) + ((slog-handler) ((await-handler) ((http-production-handler :client-factory (fn [] client) :sleep (record-sleep sleeps)) (do! @@ -167,8 +162,7 @@ (make-response 200 {} b"ok" "ok" "https://example.test/api" 0.1)])) (setv sleeps []) (<- response - (WithHandler (slog-handler) - (WithHandler (await-handler) + ((slog-handler) ((await-handler) ((http-production-handler :client-factory (fn [] client) :sleep (record-sleep sleeps)) (do! @@ -189,8 +183,7 @@ [(make-response 200 {"X-Fixture" "yes"} b"fixture" "fixture" "https://example.test/resource" 0.3)])) (<- recorded - (WithHandler (slog-handler) - (WithHandler (await-handler) + ((slog-handler) ((await-handler) ((http-fixture-handler fixture-path :mode "record" :client-factory (fn [] client) :sleep noop-sleep) diff --git a/tests/effects/test_env_var_ask.py b/tests/effects/test_env_var_ask.py index 303c0b931..746cfa24b 100644 --- a/tests/effects/test_env_var_ask.py +++ b/tests/effects/test_env_var_ask.py @@ -18,7 +18,8 @@ from doeff_core_effects.handlers import env_var_ask, lazy_ask from doeff_core_effects.scheduler import Gather, Spawn, scheduled -from doeff import Ask, Pass, Resume, WithHandler, do, run +from doeff import Ask, Pass, Resume, do, run +from doeff import handler as _install_raw_handler # --- Plain string values --------------------------------------------------- @@ -31,7 +32,7 @@ def test_resolves_string_value_from_environ(self, monkeypatch): def prog(): return (yield Ask("OPENAI_API_KEY")) - result = run(scheduled(WithHandler(env_var_ask(), prog()))) + result = run(scheduled(env_var_ask()(prog()))) assert result == "sk-abc" def test_respects_custom_prefix(self, monkeypatch): @@ -42,7 +43,7 @@ def prog(): return (yield Ask("DB_URL")) result = run( - scheduled(WithHandler(env_var_ask(prefix="NAKAGAWA_"), prog())) + scheduled(env_var_ask(prefix="NAKAGAWA_")(prog())) ) assert result == "postgres://..." @@ -59,7 +60,7 @@ def outer(effect, k): def prog(): return (yield Ask("MODEL")) - composed = WithHandler(outer, WithHandler(env_var_ask(), prog())) + composed = _install_raw_handler(outer)(env_var_ask()(prog())) assert run(scheduled(composed)) == "fallback-model" def test_dynamic_recheck_on_every_ask(self, monkeypatch): @@ -75,7 +76,7 @@ def prog(): second = yield Ask("LOG_LEVEL") return (first, second) - result = run(scheduled(WithHandler(env_var_ask(), prog()))) + result = run(scheduled(env_var_ask()(prog()))) assert result == ("info", "debug") @@ -116,7 +117,7 @@ def test_braces_import_plain_object(self, monkeypatch): def prog(): return (yield Ask("FAVORITE_NUMBER")) - assert run(scheduled(WithHandler(env_var_ask(), prog()))) == 12345 + assert run(scheduled(env_var_ask()(prog()))) == 12345 def test_braces_import_evaluates_program(self, monkeypatch): monkeypatch.setenv( @@ -129,7 +130,7 @@ def prog(): return (yield Ask("GREETING")) assert ( - run(scheduled(WithHandler(env_var_ask(), prog()))) == "pure-result" + run(scheduled(env_var_ask()(prog()))) == "pure-result" ) def test_lazy_program_uses_inner_handler_chain(self, monkeypatch): @@ -144,10 +145,7 @@ def test_lazy_program_uses_inner_handler_chain(self, monkeypatch): def prog(): return (yield Ask("OUTER")) - composed = WithHandler( - env_var_ask(), - WithHandler(lazy_ask(env={"INNER_KEY": "hello"}), prog()), - ) + composed = env_var_ask()(lazy_ask(env={"INNER_KEY": "hello"})(prog())) assert run(scheduled(composed)) == "wraps:hello" def test_program_eval_is_cached_until_raw_changes(self, monkeypatch): @@ -166,7 +164,7 @@ def prog(): c = yield Ask("COUNTER") return (a, b, c) - result = run(scheduled(WithHandler(env_var_ask(), prog()))) + result = run(scheduled(env_var_ask()(prog()))) assert result == (1, 1, 1) assert _mod._counter["n"] == 1 @@ -189,7 +187,7 @@ def prog(): second = yield Ask("COUNTER") return (first, second) - result = run(scheduled(WithHandler(env_var_ask(), prog()))) + result = run(scheduled(env_var_ask()(prog()))) assert result == (1, 2) assert _mod._counter["n"] == 2 @@ -223,7 +221,7 @@ def prog(): results = yield Gather(*tasks) return tuple(results) - results = run(scheduled(WithHandler(env_var_ask(), prog()))) + results = run(scheduled(env_var_ask()(prog()))) # Every worker resolves to some counter value, and no more than a # handful of them re-evaluate (caching takes over quickly). assert len(results) == 8 @@ -244,10 +242,7 @@ def test_pass_on_miss_by_default(self, monkeypatch): def prog(): return (yield Ask("FALLBACK_KEY")) - composed = WithHandler( - env_var_ask(), - WithHandler(lazy_ask(env={}), prog()), - ) + composed = env_var_ask()(lazy_ask(env={})(prog())) assert run(scheduled(composed)) == "fallback-value" def test_strict_mode_still_throws(self): @@ -257,6 +252,6 @@ def test_strict_mode_still_throws(self): def prog(): return (yield Ask("MISSING")) - composed = WithHandler(lazy_ask(env={}, strict=True), prog()) + composed = lazy_ask(env={}, strict=True)(prog()) with pytest.raises(KeyError): run(scheduled(composed)) diff --git a/tests/effects/test_handler_bind_exception_propagation.py b/tests/effects/test_handler_bind_exception_propagation.py index 2833e1eb2..83e1d5d1c 100644 --- a/tests/effects/test_handler_bind_exception_propagation.py +++ b/tests/effects/test_handler_bind_exception_propagation.py @@ -16,7 +16,8 @@ import pytest -from doeff import EffectBase, Pass, Resume, UnhandledEffect, WithHandler, do, run +from doeff import EffectBase, Pass, Resume, UnhandledEffect, do, run +from doeff import handler as _program_handler class _MyEffect(EffectBase): @@ -31,7 +32,7 @@ def _wrap(program, *handlers): """Wrap program with handlers (first handler is outermost).""" wrapped = program for h in reversed(handlers): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) return wrapped diff --git a/tests/effects/test_lazy_ask.py b/tests/effects/test_lazy_ask.py index dc0c58d98..d90bf1e91 100644 --- a/tests/effects/test_lazy_ask.py +++ b/tests/effects/test_lazy_ask.py @@ -5,7 +5,6 @@ from doeff_core_effects.scheduler import Gather, Spawn, Wait, scheduled from doeff import do, run -from doeff.program import WithHandler def run_with_lazy(program, env=None): @@ -18,8 +17,8 @@ def run_with_lazy(program, env=None): """ if env is None: env = {} - body = WithHandler(try_handler, program) - body = WithHandler(lazy_ask(env=env), body) + body = try_handler(program) + body = lazy_ask(env=env)(body) body = scheduled(body) return run(body) @@ -365,8 +364,8 @@ def test_missing_key_error_strict(self): def program(): return (yield Try(Ask("missing"))) - body = WithHandler(try_handler, program()) - body = WithHandler(lazy_ask(env={}, strict=True), body) + body = try_handler(program()) + body = lazy_ask(env={}, strict=True)(body) result = run(scheduled(body)) assert hasattr(result, "error") assert isinstance(result.error, KeyError) diff --git a/tests/effects/test_lazy_ask_env_var_fallback.py b/tests/effects/test_lazy_ask_env_var_fallback.py index c72b5e568..b61dd546b 100644 --- a/tests/effects/test_lazy_ask_env_var_fallback.py +++ b/tests/effects/test_lazy_ask_env_var_fallback.py @@ -34,10 +34,10 @@ Ask, Pass, Resume, - WithHandler, do, run, ) +from doeff import handler as _install_raw_handler # --- env_var_fallback_handler: resolves Ask from os.environ, passes otherwise --- @@ -75,13 +75,7 @@ def program(): env = {"creds": some_program()} - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - env_var_fallback_handler, - program(), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(env_var_fallback_handler)(program())) result = run(scheduled(composed)) assert result == "Credentials(/etc/secrets/creds.json)" @@ -104,13 +98,7 @@ def program(): "project_id": "my-project-123", } - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - env_var_fallback_handler, - program(), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(env_var_fallback_handler)(program())) result = run(scheduled(composed)) assert result == "project=my-project-123" @@ -134,13 +122,7 @@ def program(): "api_url": "https://lazy-ask.example.com", } - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - env_var_fallback_handler, - program(), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(env_var_fallback_handler)(program())) result = run(scheduled(composed)) # Inner handler (env_var_fallback) is closer -> resolves from os.environ assert result == "url=https://env.example.com" @@ -174,13 +156,7 @@ def program(): "path_key": lazy_path(), } - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - env_var_fallback_handler, - program(), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(env_var_fallback_handler)(program())) result = run(scheduled(composed)) assert result == "Credentials(/opt/secrets/creds.json)" @@ -210,13 +186,6 @@ def program(): "name": "test-service", } - composed = WithHandler( - lazy_ask(env=env), - WithHandler(writer(), - WithHandler(try_handler, - WithHandler(state(), - WithHandler(env_var_fallback_handler, - program())))), - ) + composed = lazy_ask(env=env)(writer()(try_handler(state()(_install_raw_handler(env_var_fallback_handler)(program()))))) result = run(scheduled(composed)) assert result == "loaded:/run/secrets/api-key|test-service" diff --git a/tests/effects/test_lazy_ask_inner_handler_propagation.py b/tests/effects/test_lazy_ask_inner_handler_propagation.py index 4cb9a696b..cd9293557 100644 --- a/tests/effects/test_lazy_ask_inner_handler_propagation.py +++ b/tests/effects/test_lazy_ask_inner_handler_propagation.py @@ -41,10 +41,10 @@ EffectBase, Pass, Resume, - WithHandler, do, run, ) +from doeff import handler as _install_raw_handler # --- Custom effects simulating GCP Secret Manager --- @@ -95,13 +95,7 @@ def program(): env = {"token": _fetch_secret_program()} - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - secret_handler, - program(), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(secret_handler)(program())) result = run(scheduled(composed)) assert result == "secret:my-api-token" @@ -121,14 +115,7 @@ def program(): "plain_value": "hello", } - composed = WithHandler( - lazy_ask(env=env), - WithHandler(writer(), - WithHandler(try_handler, - WithHandler(state(), - WithHandler(secret_handler, - program())))), - ) + composed = lazy_ask(env=env)(writer()(try_handler(state()(_install_raw_handler(secret_handler)(program()))))) result = run(scheduled(composed)) assert result == "secret:my-api-token|hello" @@ -154,13 +141,7 @@ def program(): "project_id": "my-project-123", } - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - secret_handler_with_ask, - program(), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(secret_handler_with_ask)(program())) result = run(scheduled(composed)) assert result == "secret:my-project-123:my-api-token" @@ -199,13 +180,7 @@ def program(): "c": lazy_c(), } - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - secret_handler, - program(), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(secret_handler)(program())) result = run(scheduled(composed)) assert result == "secret:deep-secret" @@ -239,13 +214,7 @@ def program(): "plain": "hello", } - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - secret_handler, - program(), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(secret_handler)(program())) result = run(scheduled(composed)) assert result == "secret:mixed-secret+hello" @@ -292,16 +261,7 @@ def program(): "c": lazy_c(), } - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - secret_handler, - WithHandler( - transform_handler, - program(), - ), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(secret_handler)(_install_raw_handler(transform_handler)(program()))) result = run(scheduled(composed)) # GetSecret("final") → "secret:final", Transform("secret:final") → "SECRET:FINAL" assert result == "SECRET:FINAL" @@ -341,13 +301,7 @@ def program(): "c": lazy_c(), } - composed = WithHandler( - lazy_ask(env=env), - WithHandler( - secret_handler, - program(), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(secret_handler)(program())) result = run(scheduled(composed)) assert result == "a:secret:shared|b:secret:shared" assert eval_count["c"] == 1, f"lazy_c evaluated {eval_count['c']} times, expected 1" @@ -365,15 +319,6 @@ def program(): env = {"token": _fetch_secret_program()} # Workaround: two lazy_asks with same env - composed = WithHandler( - lazy_ask(env=env), # outer — catches secret_handler's Ask - WithHandler( - secret_handler, # handles GetSecret from inner lazy_ask's Expand - WithHandler( - lazy_ask(env=env), # inner — catches program's Ask, Expands - program(), - ), - ), - ) + composed = lazy_ask(env=env)(_install_raw_handler(secret_handler)(lazy_ask(env=env)(program()))) result = run(scheduled(composed)) assert result == "secret:my-api-token" diff --git a/tests/effects/test_local_handler_ask.py b/tests/effects/test_local_handler_ask.py index 22104e73a..435108275 100644 --- a/tests/effects/test_local_handler_ask.py +++ b/tests/effects/test_local_handler_ask.py @@ -11,9 +11,9 @@ Pass, Resume, Tell, - WithHandler, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults @@ -30,7 +30,7 @@ def _run_with_handlers( ): wrapped = program for h in extra_handlers: - wrapped = WithHandler(h, wrapped) + wrapped = _install_raw_handler(h)(wrapped) return run_with_defaults(wrapped, env=env) @@ -223,10 +223,7 @@ def body(): yield Tell({"msg": "slog"}) return (yield ReplaceAudioTrackForLocalTypedFilter(duck_original=0.25, duck_speech=0.6)) - wrapped = WithHandler( - replace_audio_handler, - WithHandler(memo_rewriter, Local({"unused": "value"}, body())), - ) + wrapped = _install_raw_handler(replace_audio_handler)(_install_raw_handler(memo_rewriter)(Local({"unused": "value"}, body()))) result = run_with_defaults(wrapped, env={}) assert result.is_ok() diff --git a/tests/effects/test_memo_no_terminal.py b/tests/effects/test_memo_no_terminal.py index da029dc77..f90392d83 100644 --- a/tests/effects/test_memo_no_terminal.py +++ b/tests/effects/test_memo_no_terminal.py @@ -3,13 +3,14 @@ from doeff_core_effects.memo_handlers import in_memory_memo_handler from doeff_core_effects.scheduler import scheduled -from doeff import WithHandler, do, run +from doeff import do, run +from doeff import handler as _program_handler def _with_handlers(program, *handlers): wrapped = program for handler in reversed(handlers): - wrapped = WithHandler(handler, wrapped) + wrapped = _program_handler(handler)(wrapped) return wrapped diff --git a/tests/effects/test_memo_rewriter_compute_unhandled.py b/tests/effects/test_memo_rewriter_compute_unhandled.py index 7c00bd798..8ee4ba751 100644 --- a/tests/effects/test_memo_rewriter_compute_unhandled.py +++ b/tests/effects/test_memo_rewriter_compute_unhandled.py @@ -4,7 +4,8 @@ from doeff_core_effects.handlers import slog_handler from doeff_core_effects.memo_handlers import make_memo_rewriter -from doeff import EffectBase, UnhandledEffect, WithHandler, do, run +from doeff import EffectBase, UnhandledEffect, do, run +from doeff import handler as _program_handler @dataclass(frozen=True) @@ -15,7 +16,7 @@ class Lookup(EffectBase): def _with_handlers(program, *handlers): wrapped = program for handler in reversed(handlers): - wrapped = WithHandler(handler, wrapped) + wrapped = _program_handler(handler)(wrapped) return wrapped diff --git a/tests/effects/test_memo_rewriter_no_terminal.py b/tests/effects/test_memo_rewriter_no_terminal.py index c62ce86c8..dd61315f3 100644 --- a/tests/effects/test_memo_rewriter_no_terminal.py +++ b/tests/effects/test_memo_rewriter_no_terminal.py @@ -4,7 +4,8 @@ from doeff_core_effects.memo_handlers import in_memory_memo_handler, make_memo_rewriter from doeff_core_effects.scheduler import scheduled -from doeff import EffectBase, Pass, Resume, WithHandler, do, run +from doeff import EffectBase, Pass, Resume, do, run +from doeff import handler as _install_raw_handler @dataclass(frozen=True) @@ -15,7 +16,7 @@ class Lookup(EffectBase): def _with_handlers(program, *handlers): wrapped = program for handler in reversed(handlers): - wrapped = WithHandler(handler, wrapped) + wrapped = _install_raw_handler(handler)(wrapped) return wrapped @@ -29,7 +30,7 @@ def handler(effect, k): calls["count"] += 1 return (yield Resume(k, f"value:{effect.key}")) - return handler + return _install_raw_handler(handler) def test_memo_rewriter_uses_storage_without_terminal_and_hits_on_second_call(): diff --git a/tests/effects/test_traverse.py b/tests/effects/test_traverse.py index 4bb424bb1..f14a7df5b 100644 --- a/tests/effects/test_traverse.py +++ b/tests/effects/test_traverse.py @@ -8,7 +8,7 @@ from doeff_traverse.helpers import try_call from doeff import do, run -from doeff.program import WithHandler +from doeff import handler as _program_handler def run_with(program, handlers=None): @@ -20,12 +20,12 @@ def run_with(program, handlers=None): Extra handlers sit between try_handler and fail_handler, so they intercept Fail before fail_handler raises it. """ - body = WithHandler(try_handler, program) + body = try_handler(program) if handlers: for h in handlers: - body = WithHandler(h, body) - body = WithHandler(fail_handler, body) - body = WithHandler(sequential(), body) + body = _program_handler(h)(body) + body = fail_handler(body) + body = sequential()(body) body = scheduled(body) return run(body) diff --git a/tests/effects/test_unhandled_effect_chain.py b/tests/effects/test_unhandled_effect_chain.py index 9eb255b60..093258d29 100644 --- a/tests/effects/test_unhandled_effect_chain.py +++ b/tests/effects/test_unhandled_effect_chain.py @@ -19,7 +19,8 @@ import pytest from doeff_core_effects.handlers import lazy_ask, slog_handler -from doeff import Ask, Pass, UnhandledEffect, WithHandler, do, run +from doeff import Ask, Pass, UnhandledEffect, do, run +from doeff import handler as _install_raw_handler # Module-level handlers so their qualnames don't include ``..`` noise # from the enclosing test class/function. @@ -58,7 +59,7 @@ def prog(): return (yield Ask("missing")) with pytest.raises(UnhandledEffect) as excinfo: - run(WithHandler(only_handler, prog())) + run(_install_raw_handler(only_handler)(prog())) msg = _extract_msg(excinfo.value) assert "only_handler" in msg assert "handlers in scope" in msg @@ -71,13 +72,7 @@ def prog(): # lazy_ask defaults to Pass-on-miss (PR D), so Ask bubbles through # telemetry_handler → slog_handler → lazy_ask before going Unhandled. - composed = WithHandler( - lazy_ask(env={}), - WithHandler( - slog_handler(), - WithHandler(telemetry_handler, prog()), - ), - ) + composed = lazy_ask(env={})(slog_handler()(_install_raw_handler(telemetry_handler)(prog()))) with pytest.raises(UnhandledEffect) as excinfo: run(composed) msg = _extract_msg(excinfo.value) @@ -92,10 +87,7 @@ def test_pass_fallthrough_preserves_order(self): def prog(): return (yield Ask("X")) - composed = WithHandler( - outer_pass_through, - WithHandler(inner_pass_through, prog()), - ) + composed = _install_raw_handler(outer_pass_through)(_install_raw_handler(inner_pass_through)(prog())) with pytest.raises(UnhandledEffect) as excinfo: run(composed) msg = _extract_msg(excinfo.value) diff --git a/tests/effects/test_unhandled_effect_class.py b/tests/effects/test_unhandled_effect_class.py index e27c8ee9e..e7fb9ba4a 100644 --- a/tests/effects/test_unhandled_effect_class.py +++ b/tests/effects/test_unhandled_effect_class.py @@ -6,7 +6,8 @@ from doeff_vm import EffectBase from doeff_vm import UnhandledEffect as VmUnhandledEffect -from doeff import Pass, UnhandledEffect, WithHandler, do, run +from doeff import Pass, UnhandledEffect, do, run +from doeff import handler as _install_raw_handler @dataclass(frozen=True, kw_only=True) @@ -42,7 +43,7 @@ def program(): yield MissingEffect(label="pass-fallthrough") with pytest.raises(UnhandledEffect): - run(WithHandler(pass_through, program())) + run(_install_raw_handler(pass_through)(program())) def test_unhandled_effect_is_catchable_inside_do(): @@ -68,7 +69,7 @@ def program(): except UnhandledEffect: return "fallback" - assert run(WithHandler(pass_through, program())) == "fallback" + assert run(_install_raw_handler(pass_through)(program())) == "fallback" def test_unhandled_effect_preserves_doeff_traceback(): @@ -77,7 +78,7 @@ def program(): yield MissingEffect(label="traceback") with pytest.raises(UnhandledEffect) as exc_info: - run(WithHandler(pass_through, program())) + run(_install_raw_handler(pass_through)(program())) tb = getattr(exc_info.value, "__doeff_traceback__", None) assert isinstance(tb, list) diff --git a/tests/fixtures/repro_387.hy b/tests/fixtures/repro_387.hy index cdbb57195..de49c3aab 100644 --- a/tests/fixtures/repro_387.hy +++ b/tests/fixtures/repro_387.hy @@ -8,7 +8,7 @@ (require doeff-hy.macros [defk <-]) (require doeff-hy.handle [defhandler]) -(import doeff [Pass Resume WithHandler do :as _doeff-do]) +(import doeff [Pass Resume do :as _doeff-do]) (import doeff [EffectBase]) (import dataclasses [dataclass]) diff --git a/tests/fixtures/repro_387_no_require.hy b/tests/fixtures/repro_387_no_require.hy index 48465d269..286495af2 100644 --- a/tests/fixtures/repro_387_no_require.hy +++ b/tests/fixtures/repro_387_no_require.hy @@ -11,7 +11,7 @@ (require doeff-hy.macros [defk <-]) ;; NOTE: intentionally NOT requiring defhandler -(import doeff [Pass Resume WithHandler do :as _doeff-do]) +(import doeff [Pass Resume do :as _doeff-do]) (import doeff [EffectBase]) (import dataclasses [dataclass]) diff --git a/tests/fixtures/repro_387_via_macros.hy b/tests/fixtures/repro_387_via_macros.hy index 418e825e6..4627fcf93 100644 --- a/tests/fixtures/repro_387_via_macros.hy +++ b/tests/fixtures/repro_387_via_macros.hy @@ -3,7 +3,7 @@ (require doeff-hy.macros [defk <- defhandler]) -(import doeff [Pass Resume WithHandler do :as _doeff-do]) +(import doeff [Pass Resume do :as _doeff-do]) (import doeff [EffectBase]) (import dataclasses [dataclass]) diff --git a/tests/public_api/test_do_bang_defp_defpp.py b/tests/public_api/test_do_bang_defp_defpp.py index 6c0dfda1f..59dd9de47 100644 --- a/tests/public_api/test_do_bang_defp_defpp.py +++ b/tests/public_api/test_do_bang_defp_defpp.py @@ -13,7 +13,7 @@ import pytest from doeff_core_effects import reader -from doeff import Ask, WithHandler, do, run +from doeff import Ask, do, run # --------------------------------------------------------------------------- # do! — returns a Program (generator), usable anywhere @@ -29,7 +29,7 @@ def prog(): x = yield Ask("key") return x - result = run(WithHandler(reader(env={"key": "hello"}), prog())) + result = run(reader(env={"key": "hello"})(prog())) assert result == "hello" def test_do_bang_with_bang_expansion(self) -> None: @@ -68,7 +68,7 @@ def prog(): x = yield Ask("key") return x - result = run(WithHandler(reader(env={"key": "hello"}), prog())) + result = run(reader(env={"key": "hello"})(prog())) assert result == "hello" assert not inspect.isgenerator(result) @@ -82,7 +82,7 @@ def make_bad_program(): x = yield Ask("key") return (y for y in [x]) - result = run(WithHandler(reader(env={"key": "v"}), make_bad_program())) + result = run(reader(env={"key": "v"})(make_bad_program())) # The return value IS a generator — defp's guard would catch this assert inspect.isgenerator(result) @@ -105,7 +105,7 @@ def make_pp(): _x = yield Ask("key") return inner_gen() - result = run(WithHandler(reader(env={"key": "hello"}), make_pp())) + result = run(reader(env={"key": "hello"})(make_pp())) assert inspect.isgenerator(result) def test_defpp_detects_plain_return(self) -> None: @@ -116,7 +116,7 @@ def prog(): x = yield Ask("key") return x - result = run(WithHandler(reader(env={"key": "v"}), prog())) + result = run(reader(env={"key": "v"})(prog())) assert not inspect.isgenerator(result) @@ -191,7 +191,7 @@ def _import_repro(self) -> None: def test_defp_import_inside(self) -> None: """defp with (import json) before last expression.""" result = run( - WithHandler(reader(env={"test": "hello"}), self.mod.import_inside_defp) + reader(env={"test": "hello"})(self.mod.import_inside_defp) ) assert result == '{"a": "hello"}' @@ -208,7 +208,7 @@ def test_defp_import_only(self) -> None: def test_defp_multi_import(self) -> None: """defp with multiple imports in body.""" result = run( - WithHandler(reader(env={"key": "world"}), self.mod.multi_import_defp) + reader(env={"key": "world"})(self.mod.multi_import_defp) ) assert result is not None import json diff --git a/tests/public_api/test_doeff13_hang_regression.py b/tests/public_api/test_doeff13_hang_regression.py index 271164bca..2d01952af 100644 --- a/tests/public_api/test_doeff13_hang_regression.py +++ b/tests/public_api/test_doeff13_hang_regression.py @@ -21,9 +21,9 @@ Effect, EffectBase, Resume, - WithHandler, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults # --------------------------------------------------------------------------- @@ -129,7 +129,7 @@ def body(): return result def main(): - result = yield WithHandler(handler, _prog(body)) + result = yield _install_raw_handler(handler)(_prog(body)) return result run_result = _run_with_watchdog(lambda: _prog(main)) diff --git a/tests/public_api/test_types_001_handler_protocol.py b/tests/public_api/test_types_001_handler_protocol.py index a88dd4f71..6e2385e93 100644 --- a/tests/public_api/test_types_001_handler_protocol.py +++ b/tests/public_api/test_types_001_handler_protocol.py @@ -16,9 +16,9 @@ Pure, Put, Resume, - WithHandler, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults @@ -59,7 +59,7 @@ def body(): return result def main(): - result = yield WithHandler(handler, _prog(body)) + result = yield _install_raw_handler(handler)(_prog(body)) return result result = run_with_defaults(_prog(main)) @@ -85,7 +85,7 @@ def body(): return result def main(): - result = yield WithHandler(handler, _prog(body)) + result = yield _install_raw_handler(handler)(_prog(body)) return result result = run_with_defaults(_prog(main)) @@ -112,7 +112,7 @@ def body(): return x + 5 # body returns 15 def main(): - result = yield WithHandler(handler, _prog(body)) + result = yield _install_raw_handler(handler)(_prog(body)) return result result = run_with_defaults(_prog(main)) @@ -138,7 +138,7 @@ def body(): return x + 5 def main(): - result = yield WithHandler(handler, _prog(body)) + result = yield _install_raw_handler(handler)(_prog(body)) return result result = run_with_defaults(_prog(main)) @@ -179,11 +179,11 @@ def body(): return result def inner(): - result = yield WithHandler(inner_handler, _prog(body)) + result = yield _install_raw_handler(inner_handler)(_prog(body)) return result def main(): - result = yield WithHandler(outer_handler, _prog(inner)) + result = yield _install_raw_handler(outer_handler)(_prog(inner)) return result result = run_with_defaults(_prog(main)) @@ -216,12 +216,12 @@ def body(): return val def with_inner(): - result = yield WithHandler(inner_handler, _prog(body)) + result = yield _install_raw_handler(inner_handler)(_prog(body)) return result def main(): # outer doesn't get to handle CustomEffect — inner already did - result = yield WithHandler(outer_handler, _prog(with_inner)) + result = yield _install_raw_handler(outer_handler)(_prog(with_inner)) return result result = run_with_defaults(_prog(main)) @@ -248,11 +248,11 @@ def body(): return (a, b) def with_inner(): - result = yield WithHandler(inner_handler, _prog(body)) + result = yield _install_raw_handler(inner_handler)(_prog(body)) return result def main(): - result = yield WithHandler(outer_handler, _prog(with_inner)) + result = yield _install_raw_handler(outer_handler)(_prog(with_inner)) return result result = run_with_defaults(_prog(main)) @@ -288,7 +288,7 @@ def body(): return [a, b, c] def main(): - result = yield WithHandler(handler, _prog(body)) + result = yield _install_raw_handler(handler)(_prog(body)) return result result = run_with_defaults(_prog(main)) @@ -315,7 +315,7 @@ def body(): return result def main(): - result = yield WithHandler(handler, _prog(body)) + result = yield _install_raw_handler(handler)(_prog(body)) return result result = run_with_defaults(_prog(main)) @@ -346,7 +346,7 @@ def body(): return "done" def main(): - result = yield WithHandler(handler, _prog(body)) + result = yield _install_raw_handler(handler)(_prog(body)) return result result = run_with_defaults(_prog(main)) @@ -379,7 +379,7 @@ def body(): return f"{custom_val}:{state_val}" def main(): - result = yield WithHandler(handler, body()) + result = yield _install_raw_handler(handler)(body()) return result result = run_with_defaults(_prog(main), env={"api_key": "secret"}, store={"counter": 10}) diff --git a/tests/public_api/test_types_001_validation.py b/tests/public_api/test_types_001_validation.py index 97db89d6e..3c10542d6 100644 --- a/tests/public_api/test_types_001_validation.py +++ b/tests/public_api/test_types_001_validation.py @@ -21,10 +21,12 @@ Perform, Resume, Transfer, - WithHandler, do, run, ) +from doeff import ( + handler as make_handler, +) from tests._run_helpers import run_with_defaults @@ -45,18 +47,18 @@ def test_run_accepts_bare_rust_effectbase() -> None: -def test_withhandler_accepts_rust_effect_expr() -> None: +def test_handler_installer_accepts_rust_effect_expr() -> None: @do - def handler(_effect: Effect, _k): + def raw_handler(_effect: Effect, _k): yield _effect - ctrl = WithHandler(handler, Perform(Ask("key"))) + ctrl = make_handler(raw_handler)(Perform(Ask("key"))) assert type(ctrl).__name__ == "WithHandler" -def test_withhandler_rejects_return_clause_keyword() -> None: +def test_handler_installer_rejects_return_clause_keyword() -> None: @do - def handler(_effect: Effect, _k): + def raw_handler(_effect: Effect, _k): yield _effect @do @@ -64,11 +66,11 @@ def body(): return "ok" yield - with_handler = cast(Any, WithHandler) + install = cast(Any, make_handler(raw_handler)) kwargs = {"return_clause": lambda value: value} with pytest.raises(TypeError, match=r"return_clause|unexpected keyword"): - with_handler(handler, body(), **kwargs) + install(body(), **kwargs) def test_doeff_vm_withhandler_rejects_return_clause_keyword() -> None: @@ -90,16 +92,16 @@ def body(): with_handler(handler, body(), **kwargs) -def test_withhandler_rejects_third_positional_argument() -> None: +def test_handler_installer_rejects_second_positional_argument() -> None: @do - def handler(_effect: Effect, _k): + def raw_handler(_effect: Effect, _k): yield _effect - args = (handler, Perform(Ask("key")), lambda value: value) - with_handler = cast(Any, WithHandler) + args = (Perform(Ask("key")), lambda value: value) + install = cast(Any, make_handler(raw_handler)) with pytest.raises(TypeError, match=r"positional arguments|given"): - with_handler(*args) + install(*args) @@ -107,12 +109,12 @@ def test_python_handler_receives_k_for_resume() -> None: seen: dict[str, bool] = {"is_k": False} @do - def handler(_effect: Effect, k): + def raw_handler(_effect: Effect, k): seen["is_k"] = isinstance(k, K) return (yield Resume(k, "override")) result = run_with_defaults( - WithHandler(handler, Perform(Ask("x"))), + make_handler(raw_handler)(Perform(Ask("x"))), env={"x": "original"}, ) @@ -129,7 +131,7 @@ def transfer_handler(_effect: Effect, k): yield Transfer(k, "via-transfer") transfer_result = run_with_defaults( - WithHandler(transfer_handler, Perform(Ask("x"))), + make_handler(transfer_handler)(Perform(Ask("x"))), env={"x": "original"}, ) @@ -141,7 +143,7 @@ def delegate_handler(_effect: Effect, k): yield Pass(_effect, k) delegate_result = run_with_defaults( - WithHandler(delegate_handler, Perform(Ask("x"))), + make_handler(delegate_handler)(Perform(Ask("x"))), env={"x": "original"}, ) diff --git a/tests/test_await_http_memory.py b/tests/test_await_http_memory.py index 97e89103e..c41ae9bd6 100644 --- a/tests/test_await_http_memory.py +++ b/tests/test_await_http_memory.py @@ -17,6 +17,8 @@ import signal from typing import Any +from doeff_core_effects.cache_handlers import sqlite_cache_handler + from doeff import ( AcquireSemaphore, Await, @@ -24,15 +26,11 @@ Gather, ReleaseSemaphore, Spawn, - WithHandler, do, slog, ) from tests._run_helpers import run_with_defaults -# REMOVED: from doeff_core_effects.handlers import sqlite_cache_handler - - TIMEOUT_SECONDS = 120 @@ -102,10 +100,7 @@ def _run_test(factory, n: int, conc: int = 40): old = signal.signal(signal.SIGALRM, _timeout_handler) signal.alarm(TIMEOUT_SECONDS) try: - prog = WithHandler( - sqlite_cache_handler(None), # noqa: F821 - legacy removed API reference is intentionally preserved - _spawn_gather(factory, n, conc), - ) + prog = sqlite_cache_handler(None)(_spawn_gather(factory, n, conc)) rss_before = _rss_mb() r = run_with_defaults(prog) rss_after = _rss_mb() diff --git a/tests/test_cache_await_spawn_hang.py b/tests/test_cache_await_spawn_hang.py index 83f873712..deb1d2204 100644 --- a/tests/test_cache_await_spawn_hang.py +++ b/tests/test_cache_await_spawn_hang.py @@ -16,7 +16,7 @@ from doeff_core_effects.cache import cache from doeff_core_effects.memo_handlers import sqlite_memo_handler -from doeff import Await, Gather, Spawn, WithHandler, do +from doeff import Await, Gather, Spawn, do from tests._run_helpers import run_with_defaults @@ -42,7 +42,7 @@ def _spawn_gather_n(factory: Callable[[int], Any], total: int) -> Any: def _run_cached_spawn_gather(db_path: Path, total: int) -> list[str]: - program: Any = WithHandler(sqlite_memo_handler(db_path), _spawn_gather_n(_cached_task, total)) + program: Any = sqlite_memo_handler(db_path)(_spawn_gather_n(_cached_task, total)) result: Any = run_with_defaults(program) if result.is_err(): raise result.error diff --git a/tests/test_cli_env_merge_with_program.py b/tests/test_cli_env_merge_with_program.py index b4317e38a..9437b7dc5 100644 --- a/tests/test_cli_env_merge_with_program.py +++ b/tests/test_cli_env_merge_with_program.py @@ -11,7 +11,7 @@ from doeff_core_effects.handlers import local_handler -from doeff import Pure, WithHandler, run +from doeff import Pure, run from doeff.cli.discovery import StandardEnvMerger @@ -49,7 +49,7 @@ def load_symbol(self, path): # 2. Install local_handler when running the merge program # Demonstrate the fix: wrap with local_handler - result = run(WithHandler(local_handler, merged_program)) + result = run(local_handler(merged_program)) assert result == {"key_a": 1, "key_b": 2}, f"Got: {result}" raise AssertionError( diff --git a/tests/test_core_effects.py b/tests/test_core_effects.py index ece3183fd..c78fb1013 100644 --- a/tests/test_core_effects.py +++ b/tests/test_core_effects.py @@ -3,7 +3,7 @@ from doeff_core_effects.effects import Ask, Get, Put, Slog, Tell, Try from doeff_core_effects.handlers import reader, slog_handler, state, try_handler, writer -from doeff import Pure, WithHandler, do +from doeff import Pure, do from doeff import run as doeff_run @@ -13,7 +13,7 @@ def test_ask_returns_env_value(self): def body(): return (yield Ask("name")) - result = doeff_run(WithHandler(reader(env={"name": "Alice"}), body())) + result = doeff_run(reader(env={"name": "Alice"})(body())) assert result == "Alice" def test_ask_missing_key_raises(self): @@ -24,7 +24,7 @@ def body(): return (yield Ask("missing")) with pytest.raises(KeyError, match="missing"): - doeff_run(WithHandler(reader(env={}), body())) + doeff_run(reader(env={})(body())) def test_ask_multiple_keys(self): @do @@ -33,7 +33,7 @@ def body(): b = yield Ask("y") return a + b - result = doeff_run(WithHandler(reader(env={"x": 10, "y": 20}), body())) + result = doeff_run(reader(env={"x": 10, "y": 20})(body())) assert result == 30 @@ -46,7 +46,7 @@ def body(): yield Put("count", c + 1) return (yield Get("count")) - result = doeff_run(WithHandler(state(), body())) + result = doeff_run(state()(body())) assert result == 1 def test_initial_state(self): @@ -54,7 +54,7 @@ def test_initial_state(self): def body(): return (yield Get("x")) - result = doeff_run(WithHandler(state(initial={"x": 42}), body())) + result = doeff_run(state(initial={"x": 42})(body())) assert result == 42 def test_get_missing_returns_none(self): @@ -62,7 +62,7 @@ def test_get_missing_returns_none(self): def body(): return (yield Get("missing")) - result = doeff_run(WithHandler(state(), body())) + result = doeff_run(state()(body())) assert result is None @@ -76,7 +76,7 @@ def body(): yield Tell("world") return "done" - result = doeff_run(WithHandler(w, body())) + result = doeff_run(w(body())) assert result == "done" assert w.log == ["hello", "world"] @@ -92,10 +92,7 @@ def body(): yield Put("total", total + 10) return (yield Get("total")) - prog = WithHandler( - reader(env={"base": 100}), - WithHandler(state(), body()), - ) + prog = reader(env={"base": 100})(state()(body())) assert doeff_run(prog) == 110 def test_all_three(self): @@ -109,10 +106,7 @@ def body(): yield Put("greeted", True) return (yield Get("greeted")) - prog = WithHandler( - reader(env={"name": "Bob"}), - WithHandler(state(), WithHandler(w, body())), - ) + prog = reader(env={"name": "Bob"})(state()(w(body()))) assert doeff_run(prog) is True assert w.log == ["hello Bob"] @@ -125,7 +119,7 @@ def body(): return result from doeff_vm import Ok - result = doeff_run(WithHandler(try_handler, body())) + result = doeff_run(try_handler(body())) assert isinstance(result, Ok.__class__) or (hasattr(result, "is_ok") and result.is_ok()) assert result.value == 42 @@ -140,7 +134,7 @@ def body(): result = yield Try(failing()) return result - result = doeff_run(WithHandler(try_handler, body())) + result = doeff_run(try_handler(body())) assert hasattr(result, "is_err") assert result.is_err() assert isinstance(result.error, ValueError) @@ -157,7 +151,7 @@ def body(): yield Try(failing()) return "safe" - assert doeff_run(WithHandler(try_handler, body())) == "safe" + assert doeff_run(try_handler(body())) == "safe" class TestSlog: @@ -170,7 +164,7 @@ def body(): yield Slog("event", user="alice", action="login") return "done" - result = doeff_run(WithHandler(sh, body())) + result = doeff_run(sh(body())) assert result == "done" assert len(sh.log) == 2 assert sh.log[0] == {"msg": "hello"} @@ -194,7 +188,7 @@ def body(): result = yield Await(async_add(3, 4)) return result - result = doeff_run(scheduled(WithHandler(await_handler(), body()))) + result = doeff_run(scheduled(await_handler()(body()))) assert result == 7 def test_await_multiple(self): @@ -214,7 +208,7 @@ def body(): b = yield Await(fetch(2)) return a + b - result = doeff_run(scheduled(WithHandler(await_handler(), body()))) + result = doeff_run(scheduled(await_handler()(body()))) assert result == 30 def test_await_100_concurrent_tasks(self): @@ -239,7 +233,7 @@ def task(x): def body(): tasks = [] for i in range(100): - tasks.append((yield Spawn(WithHandler(ah, task(i))))) + tasks.append((yield Spawn(ah(task(i))))) return (yield Gather(*tasks)) start = time.time() diff --git a/tests/test_deep_handler.py b/tests/test_deep_handler.py index 49e249366..20ef63878 100644 --- a/tests/test_deep_handler.py +++ b/tests/test_deep_handler.py @@ -10,7 +10,8 @@ from doeff_core_effects.handlers import reader from doeff_vm import EffectBase -from doeff import Pass, Resume, Transfer, WithHandler, do, run +from doeff import Pass, Resume, Transfer, do, run +from doeff import handler as _install_raw_handler def test_reader_handles_multiple_asks(): @@ -24,7 +25,7 @@ def prog(): return f"{a}-{b}-{c}" env = {"key_a": "hello", "key_b": "world", "key_c": "!"} - result = run(WithHandler(reader(env=env), prog())) + result = run(reader(env=env)(prog())) assert result == "hello-world-!" @@ -53,7 +54,7 @@ def prog(): c = yield MyEffect(3) return a + b + c - result = run(WithHandler(handler, prog())) + result = run(_install_raw_handler(handler)(prog())) assert result == 60 # 10 + 20 + 30 assert call_count == 3 # handler called 3 times @@ -80,7 +81,7 @@ def prog(): return f"{g} / {f}" env = {"name": "Alice", "greeting": "Hello", "farewell": "Bye"} - result = run(WithHandler(reader(env=env), prog())) + result = run(reader(env=env)(prog())) assert result == "Hello, Alice / Bye, Alice" @@ -108,6 +109,6 @@ def prog(): a = yield Ask("x") return a - result = run(WithHandler(handler, prog())) + result = run(_install_raw_handler(handler)(prog())) assert result == "val_1" assert counter.n == 1 diff --git a/tests/test_deftest_macro.py b/tests/test_deftest_macro.py index 0f1b3cabb..7424d10da 100644 --- a/tests/test_deftest_macro.py +++ b/tests/test_deftest_macro.py @@ -18,8 +18,9 @@ import doeff_hy # noqa — registers extensions import pytest -from doeff import EffectBase, Pass, Resume, WithHandler, run +from doeff import EffectBase, Pass, Resume, run from doeff import do as _doeff_do +from doeff import handler as _program_handler # --------------------------------------------------------------------------- # Test effect + handler for use in deftest tests @@ -38,12 +39,12 @@ def _handler(effect, k): yield Resume(k, values.get(effect.key)) else: yield Pass(effect, k) - return _handler + return _program_handler(_handler) def stub_interpreter(program): """Simple test interpreter — stub handler + run.""" - return run(WithHandler(stub_handler(), program)) + return run(stub_handler()(program)) # --------------------------------------------------------------------------- diff --git a/tests/test_do_bang_setv.py b/tests/test_do_bang_setv.py index 9a926233f..d053abc13 100644 --- a/tests/test_do_bang_setv.py +++ b/tests/test_do_bang_setv.py @@ -10,14 +10,11 @@ from doeff_core_effects.handlers import lazy_ask, writer from doeff_core_effects.scheduler import scheduled -from doeff import WithHandler, do, run +from doeff import do, run def _run_program(program): - composed = WithHandler( - lazy_ask(env={}), - WithHandler(writer(), program), - ) + composed = lazy_ask(env={})(writer()(program)) return run(scheduled(composed)) diff --git a/tests/test_double_resume_traceback.py b/tests/test_double_resume_traceback.py index 2e03cf161..8059e0739 100644 --- a/tests/test_double_resume_traceback.py +++ b/tests/test_double_resume_traceback.py @@ -20,10 +20,10 @@ EffectGenerator, Pass, Resume, - WithHandler, do, run, ) +from doeff import handler as _install_raw_handler @dataclass(frozen=True) @@ -49,8 +49,7 @@ def program_single_ping() -> EffectGenerator[int]: def _run(program): - wrapped = WithHandler(writer(), WithHandler(try_handler, - WithHandler(state(), WithHandler(double_resume_handler, program)))) + wrapped = writer()(try_handler(state()(_install_raw_handler(double_resume_handler)(program)))) return run(scheduled(wrapped)) diff --git a/tests/test_error_propagation.py b/tests/test_error_propagation.py index a8f573e7b..3fcbe8dc2 100644 --- a/tests/test_error_propagation.py +++ b/tests/test_error_propagation.py @@ -5,8 +5,9 @@ from doeff_vm import EffectBase from doeff import do +from doeff import handler as _install_raw_handler from doeff import run as doeff_run -from doeff.program import Resume, WithHandler +from doeff.program import Resume class TestDirectErrors: @@ -66,7 +67,7 @@ def body(): yield Eff() with pytest.raises(ValueError, match="handler error"): - doeff_run(WithHandler(handler, body())) + doeff_run(_install_raw_handler(handler)(body())) class TestTaskErrors: diff --git a/tests/test_error_traceback_consistency.py b/tests/test_error_traceback_consistency.py index 864fac339..30869b94f 100644 --- a/tests/test_error_traceback_consistency.py +++ b/tests/test_error_traceback_consistency.py @@ -5,7 +5,8 @@ import pytest from doeff_vm import EffectBase -from doeff import Pass, WithHandler, do, run +from doeff import Pass, do, run +from doeff import handler as _install_raw_handler class Unhandled(EffectBase): @@ -47,7 +48,7 @@ def prog(): yield Unhandled() with pytest.raises(RuntimeError, match="Unhandled") as exc_info: - run(WithHandler(handler, prog())) + run(_install_raw_handler(handler)(prog())) assert_has_doeff_traceback(exc_info.value) @@ -71,7 +72,7 @@ def top(): return (yield middle()) with pytest.raises(RuntimeError, match="Unhandled") as exc_info: - run(WithHandler(handler, top())) + run(_install_raw_handler(handler)(top())) assert_has_doeff_traceback(exc_info.value, min_frames=3) @@ -101,7 +102,7 @@ def prog(): yield Unhandled() with pytest.raises(RuntimeError, match="handler crash"): - run(WithHandler(bad_handler, prog())) + run(_install_raw_handler(bad_handler)(prog())) # --- 6. Multiple handlers, inner passes, outer missing --- @@ -120,7 +121,7 @@ def prog(): yield Unhandled() with pytest.raises(RuntimeError, match="Unhandled") as exc_info: - run(WithHandler(h1, WithHandler(h2, prog()))) + run(_install_raw_handler(h1)(_install_raw_handler(h2)(prog()))) tb = getattr(exc_info.value, "__doeff_traceback__", None) assert tb is not None # Should have handler chain entries diff --git a/tests/test_execution_scope.py b/tests/test_execution_scope.py index 3a5f97bcd..fced40a5c 100644 --- a/tests/test_execution_scope.py +++ b/tests/test_execution_scope.py @@ -12,16 +12,16 @@ Resume, Transfer, Try, - WithHandler, do, ) +from doeff import handler as _program_handler from tests._run_helpers import run_with_defaults def _with_handlers(program: Any, *handlers: Any) -> Any: wrapped = program for handler in handlers: - wrapped = WithHandler(handler, wrapped) + wrapped = _program_handler(handler)(wrapped) return wrapped diff --git a/tests/test_get_handlers_defk.py b/tests/test_get_handlers_defk.py index dd110fd62..8be708746 100644 --- a/tests/test_get_handlers_defk.py +++ b/tests/test_get_handlers_defk.py @@ -24,10 +24,10 @@ from doeff import ( Pass, Resume, - WithHandler, do, run, ) +from doeff import handler as _install_raw_handler from doeff.handler_utils import get_inner_handlers # --- Effects --- @@ -66,7 +66,7 @@ def marker_handler(effect, k): def _compose(program, *handlers): wrapped = program for h in reversed(handlers): - wrapped = WithHandler(h, wrapped) + wrapped = _install_raw_handler(h)(wrapped) return wrapped @@ -92,8 +92,7 @@ def prog(): return (yield FetchData("test")) # marker_handler is between prog and catching_handler - composed = WithHandler(catching_handler, - WithHandler(marker_handler, prog())) + composed = _install_raw_handler(catching_handler)(_install_raw_handler(marker_handler)(prog())) result = run(scheduled(composed)) assert result == "caught" assert seen["handlers"] == 1, f"Expected 1 inner handler (marker), got {seen['handlers']}" diff --git a/tests/test_handle_macro.py b/tests/test_handle_macro.py index a993a5d48..8ef81b0a8 100644 --- a/tests/test_handle_macro.py +++ b/tests/test_handle_macro.py @@ -20,8 +20,9 @@ from doeff_core_effects.handlers import await_handler from doeff_core_effects.scheduler import scheduled -from doeff import EffectBase, Pass, Resume, Transfer, WithHandler, run +from doeff import EffectBase, Pass, Resume, Transfer, run from doeff import do as _doeff_do +from doeff import handler as make_handler # --------------------------------------------------------------------------- # Test effects @@ -52,7 +53,7 @@ def _handler(effect, k): yield Resume(k, effect.x + effect.y) else: yield Pass(effect, k) - return _handler + return make_handler(_handler) def store_handler(results_list): @@ -63,7 +64,7 @@ def _handler(effect, k): yield Resume(k, None) else: yield Pass(effect, k) - return _handler + return make_handler(_handler) # --------------------------------------------------------------------------- @@ -81,7 +82,6 @@ def _eval_hy(code: str, **extra_globals): "run": run, "do": _doeff_do, "_doeff_do": _doeff_do, - "WithHandler": WithHandler, "Resume": Resume, "Transfer": Transfer, "Pass": Pass, @@ -125,10 +125,7 @@ def test_bind_delegates_to_outer(self): (resume (* single 2)))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (WithHandler doubling-handler (add_program)))))))) + ((await_handler) ((store_handler results) ((real_add_handler) (doubling-handler (add_program)))))))) """, results=results) # Add(3,4) → doubling delegates → real_add gets 7 → doubles to 14 → Store(14) assert results == [14] @@ -144,10 +141,7 @@ def test_bang_in_resume(self): (! (Add :x 0 :y y)))))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (WithHandler split-handler (add_program)))))))) + ((await_handler) ((store_handler results) ((real_add_handler) (split-handler (add_program)))))))) """, results=results) # Add(3,4) → split: Add(3,0)+Add(0,4) = 3+4 = 7 → Store(7) assert results == [7] @@ -163,10 +157,7 @@ def test_bind_without_name(self): (resume (+ x y)))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (WithHandler logging-handler (add_program)))))))) + ((await_handler) ((store_handler results) ((real_add_handler) (logging-handler (add_program)))))))) """, results=results) # delegates Add(0,0) as side-effect, resumes with x+y=7 → Store(7) assert results == [7] @@ -180,10 +171,7 @@ def test_handle_with_bind(self): results = [] _eval_hy(""" (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (handle (add_program) + ((await_handler) ((store_handler results) ((real_add_handler) (handle (add_program) (Add [x y] (<- single (Add :x x :y y)) (resume (* single 3))))))))) @@ -196,10 +184,7 @@ def test_handle_with_bang(self): results = [] _eval_hy(""" (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (handle (add_program) + ((await_handler) ((store_handler results) ((real_add_handler) (handle (add_program) (Add [x y] (resume (- (! (Add :x x :y y)) 1))))))))) """, results=results) @@ -215,7 +200,7 @@ def _eval_hy_minimal(code: str, **extra_globals): """Evaluate Hy code WITHOUT pre-injecting doeff internals. Only provides user-level imports (effect types, run, scheduled). - Does NOT provide: _doeff_do, Resume, Transfer, Pass, WithHandler. + Does NOT provide: _doeff_do, Resume, Transfer, Pass. """ import types module_name = "test_handle_self_contained" @@ -294,17 +279,14 @@ def test_defhandler_parameterized_no_extra_imports(self): """) def test_defhandler_end_to_end(self): - """defhandler + run end-to-end — user only imports WithHandler for composition.""" + """defhandler + run end-to-end — handlers compose by direct calls.""" results = [] _eval_hy_minimal(""" - (import doeff [WithHandler]) (defhandler simple-add (Add [x y] (resume (+ x y)))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler simple-add (add_program)))))) + ((await_handler) ((store_handler results) (simple-add (add_program)))))) """, results=results) assert results == [7] @@ -312,14 +294,11 @@ def test_defhandler_with_transfer(self): """transfer in defhandler should also work without extra imports.""" results = [] _eval_hy_minimal(""" - (import doeff [WithHandler]) (defhandler add-and-transfer (Add [x y] (transfer (+ x y)))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler add-and-transfer (add_program)))))) + ((await_handler) ((store_handler results) (add-and-transfer (add_program)))))) """, results=results) # transfer removes add-and-transfer; Store still hits store_handler assert results == [7] @@ -338,16 +317,12 @@ class Unused(EffectBase): results = [] _eval_hy_minimal(""" - (import doeff [WithHandler]) ;; noop-handler only matches Unused — Add and Store pass through (defhandler noop-handler (Unused [] (resume None))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (WithHandler noop-handler (add_program))))))) + ((await_handler) ((store_handler results) ((real_add_handler) (noop-handler (add_program))))))) """, results=results, Unused=Unused) assert results == [7] @@ -356,9 +331,7 @@ def test_handle_inline_no_extra_imports(self): results = [] _eval_hy_minimal(""" (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (handle (add_program) + ((await_handler) ((store_handler results) (handle (add_program) (Add [x y] (resume (+ x y)))))))) """, results=results) assert results == [7] @@ -384,7 +357,6 @@ def _eval_hy_with_bind(code: str, **extra_globals): "run": run, "do": _doeff_do, "_doeff_do": _doeff_do, - "WithHandler": WithHandler, "Resume": Resume, "Transfer": Transfer, "Pass": Pass, @@ -513,21 +485,13 @@ def test_factory_handler_works_end_to_end(self): (resume (+ x y)))) _handler) - (defn init-handler [] + (defhandler init-handler "Handle Init effects (return label as confirmation)." - (_doeff-do - (fn [effect k] - (if (isinstance effect Init) - (yield (Resume k (. effect label))) - (yield (Pass effect k)))))) + (Init [label] (resume label))) (setv h (make-handler "http://test")) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (WithHandler (init-handler) - (WithHandler h (add_program)))))))) + ((await_handler) ((store_handler results) ((real_add_handler) (init-handler (h (add_program)))))))) """, results=results, Init=Init) assert results == [7] @@ -650,15 +614,11 @@ def test_reperform_forwards_to_outer(self): """(reperform effect) forwards effect+k to outer handler.""" results = [] _eval_hy(""" - (import doeff [WithHandler]) (defhandler passthrough (Add [x y] (reperform effect))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (WithHandler passthrough (add_program))))))) + ((await_handler) ((store_handler results) ((real_add_handler) (passthrough (add_program))))))) """, results=results) # passthrough reperforms Add → real_add_handler gets 7 → Store(7) assert results == [7] @@ -667,7 +627,6 @@ def test_reperform_in_conditional(self): """(reperform effect) in if branch — the #389 pattern.""" results = [] _eval_hy(""" - (import doeff [WithHandler]) (defhandler conditional-handler (Add [x y] (if (> x 10) @@ -675,10 +634,7 @@ def test_reperform_in_conditional(self): (reperform effect)))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (WithHandler conditional-handler (add_program))))))) + ((await_handler) ((store_handler results) ((real_add_handler) (conditional-handler (add_program))))))) """, results=results) # x=3, not > 10 → reperform → real_add_handler → 7 assert results == [7] @@ -710,15 +666,11 @@ def test_pass_still_works(self): """(pass) should still work (backwards compat) even if deprecated.""" results = [] _eval_hy(""" - (import doeff [WithHandler]) (defhandler old-passthrough (Add [x y] (pass))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (WithHandler old-passthrough (add_program))))))) + ((await_handler) ((store_handler results) ((real_add_handler) (old-passthrough (add_program))))))) """, results=results) assert results == [7] @@ -726,14 +678,10 @@ def test_defhandler_self_contained_no_outer_imports(self): """defhandler must work without _doeff-do or Resume/Pass in outer scope.""" results = [] _eval_hy_minimal(""" - (import doeff [WithHandler]) (defhandler passthrough (Add [x y] (reperform effect))) (run (scheduled - (WithHandler (await_handler) - (WithHandler (store_handler results) - (WithHandler (real_add_handler) - (WithHandler passthrough (add_program))))))) + ((await_handler) ((store_handler results) ((real_add_handler) (passthrough (add_program))))))) """, results=results) assert results == [7] diff --git a/tests/test_handler_chain_traversal.py b/tests/test_handler_chain_traversal.py index 89b639505..c728ed7b8 100644 --- a/tests/test_handler_chain_traversal.py +++ b/tests/test_handler_chain_traversal.py @@ -10,7 +10,8 @@ from doeff_core_effects.scheduler import scheduled from doeff_vm import EffectBase, Pass, Resume -from doeff import WithHandler, do, run +from doeff import do, run +from doeff import handler as _install_raw_handler class EffectA(EffectBase): @@ -52,18 +53,18 @@ def prog(): def test_3_handlers(): """Works: handler_a inner, noop middle, handler_b outer.""" wrapped = prog() - wrapped = WithHandler(handler_a, wrapped) - wrapped = WithHandler(noop_handler, wrapped) - wrapped = WithHandler(handler_b, wrapped) + wrapped = _install_raw_handler(handler_a)(wrapped) + wrapped = _install_raw_handler(noop_handler)(wrapped) + wrapped = _install_raw_handler(handler_b)(wrapped) assert run(wrapped) == "A(B)" def test_3_handlers_with_scheduled(): """Works with scheduled.""" wrapped = prog() - wrapped = WithHandler(handler_a, wrapped) - wrapped = WithHandler(noop_handler, wrapped) - wrapped = WithHandler(handler_b, wrapped) + wrapped = _install_raw_handler(handler_a)(wrapped) + wrapped = _install_raw_handler(noop_handler)(wrapped) + wrapped = _install_raw_handler(handler_b)(wrapped) assert run(scheduled(wrapped)) == "A(B)" @@ -77,11 +78,11 @@ def test_29_handlers(): wrapped = prog() for i in range(29): if i == 9: - wrapped = WithHandler(handler_a, wrapped) + wrapped = _install_raw_handler(handler_a)(wrapped) elif i == 18: - wrapped = WithHandler(handler_b, wrapped) + wrapped = _install_raw_handler(handler_b)(wrapped) else: - wrapped = WithHandler(noop_handler, wrapped) + wrapped = _install_raw_handler(noop_handler)(wrapped) assert run(wrapped) == "A(B)" @@ -90,9 +91,9 @@ def test_29_handlers_with_scheduled(): wrapped = prog() for i in range(29): if i == 9: - wrapped = WithHandler(handler_a, wrapped) + wrapped = _install_raw_handler(handler_a)(wrapped) elif i == 18: - wrapped = WithHandler(handler_b, wrapped) + wrapped = _install_raw_handler(handler_b)(wrapped) else: - wrapped = WithHandler(noop_handler, wrapped) + wrapped = _install_raw_handler(noop_handler)(wrapped) assert run(scheduled(wrapped)) == "A(B)" diff --git a/tests/test_handler_exception_catchable.py b/tests/test_handler_exception_catchable.py index 2dc1fb6d7..7ce1821bd 100644 --- a/tests/test_handler_exception_catchable.py +++ b/tests/test_handler_exception_catchable.py @@ -24,10 +24,10 @@ EffectBase, Pass, Try, - WithHandler, do, run, ) +from doeff import handler as _install_raw_handler @dataclass(frozen=True, kw_only=True) @@ -83,10 +83,10 @@ def body(): @do def program(): - result = yield Try(WithHandler(_plain_crashing_handler, body())) + result = yield Try(_install_raw_handler(_plain_crashing_handler)(body())) return result - result = run(WithHandler(try_handler, program())) + result = run(try_handler(program())) assert isinstance(result, Err), f"Expected Err, got {result!r}" assert isinstance(result.error, ValueError) assert "handler crashed" in str(result.error) @@ -102,10 +102,10 @@ def body(): @do def program(): - result = yield Try(WithHandler(_plain_key_error_handler, body())) + result = yield Try(_install_raw_handler(_plain_key_error_handler)(body())) return result - result = run(WithHandler(try_handler, program())) + result = run(try_handler(program())) assert isinstance(result, Err), f"Expected Err, got {result!r}" assert isinstance(result.error, KeyError) @@ -126,7 +126,7 @@ def body(): @do def program(): try: - result = yield WithHandler(_plain_crashing_handler, body()) + result = yield _install_raw_handler(_plain_crashing_handler)(body()) return result except ValueError as e: return f"caught: {e}" @@ -150,10 +150,10 @@ def body(): @do def program(): - result = yield Try(WithHandler(_do_crashing_handler, body())) + result = yield Try(_install_raw_handler(_do_crashing_handler)(body())) return result - result = run(WithHandler(try_handler, program())) + result = run(try_handler(program())) assert isinstance(result, Err), f"Expected Err, got {result!r}" assert isinstance(result.error, ValueError) assert "handler body crashed" in str(result.error) @@ -170,7 +170,7 @@ def body(): @do def program(): try: - result = yield WithHandler(_do_crashing_handler, body()) + result = yield _install_raw_handler(_do_crashing_handler)(body()) return result except ValueError as e: return f"caught: {e}" @@ -199,10 +199,10 @@ def body(): @do def program(): - result = yield Try(WithHandler(type_error_handler, body())) + result = yield Try(_install_raw_handler(type_error_handler)(body())) return result - result = run(WithHandler(try_handler, program())) + result = run(try_handler(program())) assert isinstance(result, Err) assert isinstance(result.error, TypeError), ( f"Expected TypeError, got {type(result.error).__name__}" @@ -234,11 +234,11 @@ def body(): @do def program(): - inner = WithHandler(inner_handler, body()) - result = yield Try(WithHandler(outer_handler, inner)) + inner = _install_raw_handler(inner_handler)(body()) + result = yield Try(_install_raw_handler(outer_handler)(inner)) return result - result = run(WithHandler(try_handler, program())) + result = run(try_handler(program())) assert isinstance(result, Err), f"Expected Err, got {result!r}" assert isinstance(result.error, RuntimeError) assert "outer handler failed" in str(result.error) diff --git a/tests/test_handler_nested_do.py b/tests/test_handler_nested_do.py index 1dc6b409b..0966b98b5 100644 --- a/tests/test_handler_nested_do.py +++ b/tests/test_handler_nested_do.py @@ -7,7 +7,8 @@ from doeff_core_effects.scheduler import scheduled from doeff_vm import EffectBase, Pass, Resume -from doeff import WithHandler, do, run +from doeff import do, run +from doeff import handler as _install_raw_handler class EffectA(EffectBase): @@ -48,16 +49,16 @@ def prog(): def test_nested_do_in_handler(): """Handler calls nested @do that emits effect.""" wrapped = prog() - wrapped = WithHandler(handler_a, wrapped) - wrapped = WithHandler(handler_b, wrapped) + wrapped = _install_raw_handler(handler_a)(wrapped) + wrapped = _install_raw_handler(handler_b)(wrapped) assert run(wrapped) == "A(B)" def test_nested_do_in_handler_with_scheduled(): """Same with scheduled.""" wrapped = prog() - wrapped = WithHandler(handler_a, wrapped) - wrapped = WithHandler(handler_b, wrapped) + wrapped = _install_raw_handler(handler_a)(wrapped) + wrapped = _install_raw_handler(handler_b)(wrapped) assert run(scheduled(wrapped)) == "A(B)" @@ -79,6 +80,6 @@ def handler_a_deep(effect, k): def test_deep_nested_do_in_handler(): """Two levels of @do nesting inside handler.""" wrapped = prog() - wrapped = WithHandler(handler_a_deep, wrapped) - wrapped = WithHandler(handler_b, wrapped) + wrapped = _install_raw_handler(handler_a_deep)(wrapped) + wrapped = _install_raw_handler(handler_b)(wrapped) assert run(scheduled(wrapped)) == "A(B)" diff --git a/tests/test_intercept.py b/tests/test_intercept.py index 707e944a3..c58726012 100644 --- a/tests/test_intercept.py +++ b/tests/test_intercept.py @@ -2,7 +2,8 @@ from doeff_vm import Callable, EffectBase -from doeff import WithHandler, WithObserve, do +from doeff import WithObserve, do +from doeff import handler as _install_raw_handler from doeff import run as doeff_run from doeff.program import Resume @@ -31,7 +32,7 @@ def body(): y = yield Ask("b") return (x, y) - result = doeff_run(WithHandler(handler, WithObserve(Callable(observer), body()))) + result = doeff_run(_install_raw_handler(handler)(WithObserve(Callable(observer), body()))) assert result == ("val:a", "val:b") assert seen == ["seen:a", "seen:b"] @@ -54,7 +55,7 @@ def handler(effect, k): def body(): return (yield Ask("original")) - result = doeff_run(WithHandler(handler, WithObserve(Callable(observer), body()))) + result = doeff_run(_install_raw_handler(handler)(WithObserve(Callable(observer), body()))) assert result == "original" def test_observer_with_multiple_effect_types(self): @@ -86,7 +87,7 @@ def body(): yield Log("world") return "done" - result = doeff_run(WithHandler(handler, WithObserve(Callable(observer), body()))) + result = doeff_run(_install_raw_handler(handler)(WithObserve(Callable(observer), body()))) assert result == "done" assert seen == ["Log", "Ask", "Log"] @@ -116,8 +117,7 @@ def body(): return (yield Ask("x")) result = doeff_run( - WithHandler(handler, - WithObserve(Callable(outer_obs), + _install_raw_handler(handler)(WithObserve(Callable(outer_obs), WithObserve(Callable(inner_obs), body()))) ) assert result == 42 @@ -148,7 +148,7 @@ def body(): # Observer is OUTSIDE handler — should still see the effect result = doeff_run( WithObserve(Callable(observer), - WithHandler(handler, body())) + _install_raw_handler(handler)(body())) ) assert result == "handled" assert seen == ["observed:x"] diff --git a/tests/test_llm_multi_provider_handlers.py b/tests/test_llm_multi_provider_handlers.py index 9de00dc16..6f05d59a7 100644 --- a/tests/test_llm_multi_provider_handlers.py +++ b/tests/test_llm_multi_provider_handlers.py @@ -10,6 +10,7 @@ from doeff_openrouter.handlers import MockOpenRouterRuntime, openrouter_mock_handler from pydantic import BaseModel +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults try: @@ -24,7 +25,7 @@ allow_module_level=True, ) -from doeff import Effect, EffectGenerator, WithHandler, do +from doeff import Effect, EffectGenerator, do class AnalysisResult(BaseModel): @@ -76,7 +77,7 @@ def workflow() -> EffectGenerator[dict[str, Any]]: } result = run_with_defaults( - WithHandler(gemini_handler, WithHandler(openai_handler, workflow())), + _install_raw_handler(gemini_handler)(_install_raw_handler(openai_handler)(workflow())), ) assert result.is_ok() @@ -109,7 +110,7 @@ def catch_all_handler(effect: Effect, k: Any): return (yield openrouter_mock_handler(effect, k)) result = run_with_defaults( - WithHandler(catch_all_handler, workflow()), + _install_raw_handler(catch_all_handler)(workflow()), ) assert result.is_ok() @@ -121,7 +122,7 @@ def router_handler(effect: Effect, k: Any): return (yield openrouter_mock_handler(effect, k, runtime=runtime)) routed = run_with_defaults( - WithHandler(router_handler, workflow()), + _install_raw_handler(router_handler)(workflow()), ) assert routed.is_ok() diff --git a/tests/test_macro_self_contained.py b/tests/test_macro_self_contained.py index 0025ed1a2..ec5dee971 100644 --- a/tests/test_macro_self_contained.py +++ b/tests/test_macro_self_contained.py @@ -16,7 +16,7 @@ from doeff_core_effects.handlers import await_handler, lazy_ask from doeff_core_effects.scheduler import scheduled -from doeff import EffectBase, WithHandler, run +from doeff import EffectBase, run @dataclass(frozen=True) @@ -36,7 +36,6 @@ def _eval_no_doeff_do(code: str, **extra_globals): mod.__dict__.update({ "run": run, - "WithHandler": WithHandler, "scheduled": scheduled, "await_handler": await_handler, "lazy_ask": lazy_ask, @@ -93,11 +92,10 @@ def test_do_bang_no_external_import(self): """do! should work without (import doeff [do :as _doeff-do]).""" result = _eval_no_doeff_do(""" (run (scheduled - (WithHandler (await_handler) - (WithHandler (lazy_ask :env {"key" "hello"}) + ((await_handler) ((lazy_ask :env {"key" "hello"}) (do! (<- val (Ask "key")) - (+ val " world")))))) + (+ val " world"))) ))) """) assert result == "hello world" @@ -112,9 +110,8 @@ def test_defp_no_external_import(self): (+ val " world")) (run (scheduled - (WithHandler (await_handler) - (WithHandler (lazy_ask :env {"key" "hello"}) - my-prog)))) + ((await_handler) ((lazy_ask :env {"key" "hello"}) + my-prog) ))) """) assert result == "hello world" diff --git a/tests/test_memo_rewriter_spawn_continuation.py b/tests/test_memo_rewriter_spawn_continuation.py index 546bd7499..fc5b09f8c 100644 --- a/tests/test_memo_rewriter_spawn_continuation.py +++ b/tests/test_memo_rewriter_spawn_continuation.py @@ -29,10 +29,10 @@ EffectGenerator, Pass, Resume, - WithHandler, do, run, ) +from doeff import handler as _program_handler # --- Custom effect (analogous to HistoricalPriceQuery) --- @@ -88,7 +88,7 @@ def simple_fetch_handler(effect, k): def _compose(program, *handlers): wrapped = program for h in reversed(handlers): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) return wrapped diff --git a/tests/test_new_vm.py b/tests/test_new_vm.py index c82708251..832e539fd 100644 --- a/tests/test_new_vm.py +++ b/tests/test_new_vm.py @@ -7,7 +7,8 @@ import pytest from doeff_vm import EffectBase, PyVM -from doeff import Pass, WithHandler, do +from doeff import Pass, do +from doeff import handler as _install_raw_handler from doeff import run as doeff_run from doeff.program import program @@ -118,7 +119,7 @@ def body(): result = yield Perform(Ask()) return result - assert doeff_run(WithHandler(handler, program(body))) == 100 + assert doeff_run(_install_raw_handler(handler)(program(body))) == 100 def test_perform_resume_body_transforms(self): """Body transforms the resumed value.""" @@ -134,7 +135,7 @@ def body(): x = yield Perform(Get()) return x * 2 - assert doeff_run(WithHandler(handler, program(body))) == 20 + assert doeff_run(_install_raw_handler(handler)(program(body))) == 20 def test_perform_transfer(self): """Handler transfers (tail position).""" @@ -149,7 +150,7 @@ def body(): result = yield Perform(Get()) return result - assert doeff_run(WithHandler(handler, program(body))) == 77 + assert doeff_run(_install_raw_handler(handler)(program(body))) == 77 def test_multiple_performs(self): """Body performs twice, handler handles both.""" @@ -170,7 +171,7 @@ def body(): b = yield Perform(Get()) return a + b - result = doeff_run(WithHandler(handler, program(body))) + result = doeff_run(_install_raw_handler(handler)(program(body))) assert result == 30 # 10 + 20 def test_handler_receives_effect_object(self): @@ -192,7 +193,7 @@ def body(): result = yield Perform(Add(3, 4)) return result - assert doeff_run(WithHandler(handler, program(body))) == 7 + assert doeff_run(_install_raw_handler(handler)(program(body))) == 7 def test_handler_return_value_flows_through(self): """After Resume, body's return value flows back to handler.""" @@ -211,7 +212,7 @@ def body(): x = yield Perform(Get()) return x + 1 - result = doeff_run(WithHandler(handler, program(body))) + result = doeff_run(_install_raw_handler(handler)(program(body))) assert result == 43 assert handler_saw[0] == 43 @@ -234,7 +235,7 @@ def body(): result = yield Ask("config") return result - assert doeff_run(WithHandler(handler, program(body))) == "value_for_config" + assert doeff_run(_install_raw_handler(handler)(program(body))) == "value_for_config" # --------------------------------------------------------------------------- @@ -256,7 +257,7 @@ def body(): result = yield Perform(Get()) return result - assert doeff_run(WithHandler(inner, program(body))) == 42 + assert doeff_run(_install_raw_handler(inner)(program(body))) == 42 # --------------------------------------------------------------------------- @@ -270,7 +271,7 @@ def body(): yield Perform(None) with pytest.raises(RuntimeError): - doeff_run(WithHandler(lambda _e, _k: Pure(None), program(body))) + doeff_run(_install_raw_handler(lambda _e, _k: Pure(None))(program(body))) def test_no_handler_error(self): """Performing without a handler raises an error.""" @@ -323,7 +324,7 @@ def inner(): def outer(): return (yield program(inner)) - result = doeff_run(WithHandler(handler, program(outer))) + result = doeff_run(_install_raw_handler(handler)(program(outer))) assert result == "answer" assert captured_traceback[0] is not None assert isinstance(captured_traceback[0], list) @@ -359,7 +360,7 @@ def middle(): def root(): return (yield program(middle)) - result = doeff_run(WithHandler(handler, program(root))) + result = doeff_run(_install_raw_handler(handler)(program(root))) assert result == 42 # Should see: leaf, middle, root (innermost first) func_names = [f[0] for f in captured_traceback[0]] @@ -407,7 +408,7 @@ def body(): x = yield Ask("val") return x - result = doeff_run(WithHandler(handler, body())) + result = doeff_run(_install_raw_handler(handler)(body())) assert result == 99 @@ -447,7 +448,7 @@ def handler(effect, k): def body(): return (yield Get()) - assert doeff_run(WithHandler(handler, body())) == 42 + assert doeff_run(_install_raw_handler(handler)(body())) == 42 def test_nested_pass_to_outer(self): """Inner handler passes, outer handler handles.""" @@ -469,7 +470,7 @@ def outer(effect, k): def body(): return (yield Ask("x")) - result = doeff_run(WithHandler(outer, WithHandler(inner, body()))) + result = doeff_run(_install_raw_handler(outer)(_install_raw_handler(inner)(body()))) assert result == "outer:x" def test_nested_pass_then_inner_handles(self): @@ -502,7 +503,7 @@ def body(): x = yield Ask("key") return x - prog = WithHandler(log_handler, WithHandler(ask_handler, body())) + prog = _install_raw_handler(log_handler)(_install_raw_handler(ask_handler)(body())) result = doeff_run(prog) assert result == "val:key" @@ -542,7 +543,7 @@ def body(): yield Log(f"got {x}") return x - prog = WithHandler(log_handler, WithHandler(ask_handler, body())) + prog = _install_raw_handler(log_handler)(_install_raw_handler(ask_handler)(body())) result = doeff_run(prog) assert result == "val:key" assert logged == ["starting", "got val:key"] diff --git a/tests/test_pass_dispatch_context_regression.py b/tests/test_pass_dispatch_context_regression.py index c8b00437e..c3e5863fb 100644 --- a/tests/test_pass_dispatch_context_regression.py +++ b/tests/test_pass_dispatch_context_regression.py @@ -15,7 +15,8 @@ import doeff # REMOVED: from doeff_core_effects.cache import in_memory_cache_handler, memo_rewriters -from doeff import EffectBase, WithHandler, do +from doeff import EffectBase, do +from doeff import handler as _program_handler class EffectA(EffectBase): @@ -33,5 +34,5 @@ def effect_a_handler(effect: EffectA, k: object): def _compose(program, *handlers): wrapped = program for handler in reversed(handlers): - wrapped = WithHandler(handler, wrapped) + wrapped = _program_handler(handler)(wrapped) return wrapped diff --git a/tests/test_pass_traceback.py b/tests/test_pass_traceback.py index ab800f98a..50e836759 100644 --- a/tests/test_pass_traceback.py +++ b/tests/test_pass_traceback.py @@ -10,7 +10,8 @@ import pytest from doeff_vm import EffectBase -from doeff import Pass, Resume, WithHandler, do, run +from doeff import Pass, Resume, do, run +from doeff import handler as _install_raw_handler class Handled(EffectBase): @@ -40,7 +41,7 @@ def prog(): return "unreachable" with pytest.raises(RuntimeError, match="Unhandled"): - run(WithHandler(handler, prog())) + run(_install_raw_handler(handler)(prog())) def test_pass_error_after_successful_handle(): @@ -53,7 +54,7 @@ def prog(): return x with pytest.raises(RuntimeError, match="Unhandled"): - run(WithHandler(handler, prog())) + run(_install_raw_handler(handler)(prog())) def test_no_handler_at_all(): diff --git a/tests/test_scheduled_handler_isolation.py b/tests/test_scheduled_handler_isolation.py index 005f1ae13..eab49b6fc 100644 --- a/tests/test_scheduled_handler_isolation.py +++ b/tests/test_scheduled_handler_isolation.py @@ -9,7 +9,8 @@ from doeff_core_effects.scheduler import scheduled from doeff_vm import EffectBase, Pass, Resume -from doeff import Ask, WithHandler, do, run +from doeff import Ask, do, run +from doeff import handler as _install_raw_handler class Transcribe(EffectBase): @@ -47,26 +48,26 @@ def dynamic_handler_wrapper(program): th = yield Ask(TranscribeHandlerKey) # Build handler chain dynamically wrapped = program - wrapped = WithHandler(rh, wrapped) # resolve outermost - wrapped = WithHandler(th, wrapped) # transcribe innermost + wrapped = _install_raw_handler(rh)(wrapped) # resolve outermost + wrapped = _install_raw_handler(th)(wrapped) # transcribe innermost return (yield wrapped) def test_static_handlers_with_scheduled(): """Static handler wrapping + scheduled: works.""" - sub = WithHandler(transcribe_handler, (lambda: (yield Transcribe()))()) + sub = _install_raw_handler(transcribe_handler)((lambda: (yield Transcribe()))()) @do def inner(): return (yield Transcribe()) - sub = WithHandler(transcribe_handler, inner()) + sub = _install_raw_handler(transcribe_handler)(inner()) @do def outer(): return (yield sub) - composed = WithHandler(resolve_handler, outer()) + composed = _install_raw_handler(resolve_handler)(outer()) assert run(scheduled(composed)) == "transcribed(resolved)" @@ -84,7 +85,7 @@ def inner(): return (yield Transcribe()) # Sub-program with its own handler (like _abepura_311_transcribe) - sub = WithHandler(transcribe_handler, inner()) + sub = _install_raw_handler(transcribe_handler)(inner()) @do def outer(): @@ -96,7 +97,7 @@ def outer(): } composed = dynamic_handler_wrapper(outer()) - composed = WithHandler(lazy_ask(env=env), composed) + composed = lazy_ask(env=env)(composed) assert run(scheduled(composed)) == "transcribed(resolved)" @@ -110,7 +111,7 @@ def test_dynamic_handlers_sub_emits_through_stack(): def inner(): return (yield Transcribe()) - sub = WithHandler(transcribe_handler, inner()) + sub = _install_raw_handler(transcribe_handler)(inner()) @do def program(): @@ -122,6 +123,6 @@ def program(): } composed = dynamic_handler_wrapper(program()) - composed = WithHandler(lazy_ask(env=env), composed) + composed = lazy_ask(env=env)(composed) result = run(scheduled(composed)) assert result == "transcribed(resolved)", f"got: {result}" diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index ec08f9fdf..a5d5148bd 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -24,6 +24,7 @@ ) from doeff import EffectBase, Pass, Resume, do +from doeff import handler as _install_raw_handler from doeff import run as doeff_run RACE_TIMEOUT_SECONDS = 2 @@ -546,8 +547,7 @@ def body(): results = yield Gather(ta, tb) return results - from doeff.program import WithHandler - result = doeff_run(WithHandler(log_handler, scheduled(body()))) + result = doeff_run(_install_raw_handler(log_handler)(scheduled(body()))) assert result == ["A", "B"] # Both tasks ran — all log entries present assert "a1" in log diff --git a/tests/test_segment_owned_state.py b/tests/test_segment_owned_state.py index 288a54e26..4179eba81 100644 --- a/tests/test_segment_owned_state.py +++ b/tests/test_segment_owned_state.py @@ -11,16 +11,16 @@ Pass, Resume, Try, - WithHandler, do, ) +from doeff import handler as _program_handler from tests._run_helpers import run_with_defaults def _with_handlers(program: Any, *handlers: Any) -> Any: wrapped = program for handler in handlers: - wrapped = WithHandler(handler, wrapped) + wrapped = _program_handler(handler)(wrapped) return wrapped diff --git a/tests/test_spawn_with_handlers.py b/tests/test_spawn_with_handlers.py index 046f34443..2ecbfbfbc 100644 --- a/tests/test_spawn_with_handlers.py +++ b/tests/test_spawn_with_handlers.py @@ -3,9 +3,8 @@ Spawned tasks need access to the full handler chain so that effects (Ask, Try, cache, etc.) work inside spawned programs. -Bug: `RuntimeError: VM error (non-exception value in Raise): -"generator yielded non-DoExpr: ."` -when a spawned task performs effects that require handlers. +Bug: spawned task handler propagation previously raised a non-DoExpr VM error +when a spawned task performed effects that require handlers. """ from doeff_core_effects import Ask, Tell, Try from doeff_core_effects.handlers import ( @@ -18,7 +17,8 @@ from doeff_core_effects.scheduler import Gather, Spawn, Wait, scheduled from doeff_vm import EffectBase, Ok -from doeff import Pass, Resume, WithHandler, do, run +from doeff import Pass, Resume, do, run +from doeff import handler as _program_handler # --- Custom effect + handler (mimics LLMStructuredQuery + openai_handler) --- @@ -57,7 +57,7 @@ def _run_with_handlers(program): custom_query_handler, ] for h in reversed(handlers): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) return run(scheduled(wrapped)) diff --git a/tests/test_try_deep_in_handler_body.py b/tests/test_try_deep_in_handler_body.py index 3c2d13f66..6bfb354e6 100644 --- a/tests/test_try_deep_in_handler_body.py +++ b/tests/test_try_deep_in_handler_body.py @@ -18,7 +18,8 @@ from doeff_core_effects.handlers import reader, state, try_handler, writer from doeff_vm import EffectBase, Ok -from doeff import Pass, Resume, WithHandler, do, run +from doeff import Pass, Resume, do, run +from doeff import handler as _program_handler class CustomQuery(EffectBase): @@ -116,7 +117,7 @@ def prog(): wrapped = prog() # reader(outer) → state → writer → try_handler → custom_query_handler(inner) for h in reversed([reader(env=env), state(), writer(), try_handler, custom_query_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result == "response(prebuilt-client, hello)" @@ -132,7 +133,7 @@ def prog(): env = {"api_key": "sk-fallback"} # no 'client' key wrapped = prog() for h in reversed([reader(env=env), state(), writer(), try_handler, custom_query_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result == "response(client(sk-fallback), hello)" @@ -148,7 +149,7 @@ def prog(): env = {} # no client, no api_key wrapped = prog() for h in reversed([reader(env=env), state(), writer(), try_handler, custom_query_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result == "response(client(default-key), hello)" @@ -167,7 +168,7 @@ def prog(): env = {"api_key": "sk-test"} wrapped = prog() for h in reversed([reader(env=env), state(), writer(), try_handler, custom_query_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert isinstance(result, Ok) @@ -206,7 +207,7 @@ def prog(): wrapped = prog() for h in reversed([try_handler, flaky_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result == "success on attempt 3" diff --git a/tests/test_try_finally_in_do.py b/tests/test_try_finally_in_do.py index 24b1d64f2..a79e488cc 100644 --- a/tests/test_try_finally_in_do.py +++ b/tests/test_try_finally_in_do.py @@ -17,9 +17,9 @@ Resume, Spawn, Transfer, - WithHandler, do, ) +from doeff import handler as _install_raw_handler from tests._run_helpers import run_with_defaults @@ -87,7 +87,7 @@ def program(): @do def wrapper(): - value = yield WithHandler(_resume_handler, program()) + value = yield _install_raw_handler(_resume_handler)(program()) cleaned = yield Get("cleaned") return value, cleaned @@ -107,7 +107,7 @@ def program(): @do def wrapper(): - value = yield WithHandler(_transfer_handler, program()) + value = yield _install_raw_handler(_transfer_handler)(program()) cleaned = yield Get("cleaned") return value, cleaned @@ -132,7 +132,7 @@ def program(): @do def wrapper(): yield Put("handler_cleaned", False) - value = yield WithHandler(handler, program()) + value = yield _install_raw_handler(handler)(program()) cleaned = yield Get("handler_cleaned") return value, cleaned diff --git a/tests/test_try_handler.py b/tests/test_try_handler.py index cb66b8abe..598e2292d 100644 --- a/tests/test_try_handler.py +++ b/tests/test_try_handler.py @@ -7,7 +7,7 @@ from doeff_core_effects.handlers import try_handler from doeff_vm import Err, Ok -from doeff import WithHandler, do, run +from doeff import do, run def test_try_catches_error(): @@ -22,7 +22,7 @@ def prog(): result = yield Try(failing()) return result - result = run(WithHandler(try_handler, prog())) + result = run(try_handler(prog())) assert isinstance(result, Err) assert isinstance(result.error, ValueError) @@ -39,6 +39,6 @@ def prog(): result = yield Try(succeeding()) return result - result = run(WithHandler(try_handler, prog())) + result = run(try_handler(prog())) assert isinstance(result, Ok) assert result.value == 42 diff --git a/tests/test_try_handler_scope.py b/tests/test_try_handler_scope.py index b6433bec2..0dfefbfbc 100644 --- a/tests/test_try_handler_scope.py +++ b/tests/test_try_handler_scope.py @@ -11,7 +11,8 @@ from doeff_core_effects.handlers import reader, try_handler, writer from doeff_vm import EffectBase -from doeff import Pass, Resume, WithHandler, do, run +from doeff import Pass, Resume, do, run +from doeff import handler as _program_handler class CustomEffect(EffectBase): @@ -46,7 +47,7 @@ def prog(): env = {"llm_model": "gpt-5.4"} wrapped = prog() for h in reversed([reader(env=env), try_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result.value == "model=gpt-5.4" @@ -74,7 +75,7 @@ def prog(): wrapped = prog() # reader (outer) → try_handler → custom_handler (inner) for h in reversed([reader(env=env), try_handler, custom_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result.value == "handled:gpt-5.4" @@ -95,7 +96,7 @@ def prog(): wrapped = prog() # custom_handler (outer) → try_handler (inner) for h in reversed([custom_handler, try_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result.value == "handled:test" @@ -119,7 +120,7 @@ def prog(): wrapped = prog() # reader (outer) → writer → try_handler → custom_handler (inner) for h in reversed([reader(env=env), writer(), try_handler, custom_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result.value == "handled:gpt-5.4" @@ -139,7 +140,7 @@ def prog(): wrapped = prog() for h in reversed([try_handler, custom_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result.is_err() diff --git a/tests/test_try_in_handler_body.py b/tests/test_try_in_handler_body.py index f2f4d296c..9fafb956d 100644 --- a/tests/test_try_in_handler_body.py +++ b/tests/test_try_in_handler_body.py @@ -10,7 +10,8 @@ from doeff_core_effects.handlers import reader, try_handler from doeff_vm import EffectBase -from doeff import Pass, Resume, WithHandler, do, run +from doeff import Pass, Resume, do, run +from doeff import handler as _program_handler class LLMQuery(EffectBase): @@ -53,7 +54,7 @@ def prog(): # reader (outer) → try_handler → llm_handler (inner) wrapped = prog() for h in reversed([reader(env=env), try_handler, llm_handler_that_uses_try]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result == "response from sk-test: hello" @@ -80,7 +81,7 @@ def prog(): wrapped = prog() for h in reversed([reader(env={}), try_handler, llm_handler_error]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) result = run(wrapped) assert result == "caught: True" diff --git a/tests/test_with_observe_visibility.py b/tests/test_with_observe_visibility.py index ab95d2abb..e1e53f9df 100644 --- a/tests/test_with_observe_visibility.py +++ b/tests/test_with_observe_visibility.py @@ -17,7 +17,8 @@ from doeff_vm import Callable as VMCallable from doeff_vm import EffectBase -from doeff import Pass, Resume, WithHandler, WithObserve, do, run +from doeff import Pass, Resume, WithObserve, do, run +from doeff import handler as _program_handler class CustomQuery(EffectBase): @@ -50,7 +51,7 @@ def prog(): wrapped = prog() for h in reversed([state(), writer(), slog_handler()]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) wrapped = WithObserve(VMCallable(observer), wrapped) result = run(wrapped) @@ -79,7 +80,7 @@ def prog(): wrapped = prog() for h in reversed([state(), writer(), try_handler, slog_handler(), custom_handler]): - wrapped = WithHandler(h, wrapped) + wrapped = _program_handler(h)(wrapped) wrapped = WithObserve(VMCallable(observer), wrapped) result = run(wrapped) diff --git a/tests/test_withhandler_shim_deprecation.py b/tests/test_withhandler_shim_deprecation.py deleted file mode 100644 index 2c3b47016..000000000 --- a/tests/test_withhandler_shim_deprecation.py +++ /dev/null @@ -1,97 +0,0 @@ -"""PR A4: WithHandler shim emits a DeprecationWarning. - -The shim itself stays in place (scope A — permanent shim for the legacy -@do-dispatcher form). Calling ``WithHandler(h, body)`` surfaces a -DeprecationWarning that points callers at the new-style idiom: - - # Before - WithHandler(my_handler, program) - # After (new-style defhandler / handle) - my_handler(program) - -``WithHandlerType`` (the type alias for ``isinstance`` checks and direct -DoExpr construction) is *not* deprecated — only the wrapping function. -""" - -from __future__ import annotations - -import warnings - -from doeff import Pass, Pure, WithHandler, WithHandlerType, do, run -from doeff.program import _WithHandlerNode - - -def _new_style_handler(): - """Build a minimal new-style handler: a Program -> Program function - with the PR A1 marker.""" - - def handler_fn(body): - return _WithHandlerNode(_legacy_dispatcher, body) - - handler_fn._doeff_is_handler_fn = True # type: ignore[attr-defined] - return handler_fn - - -@do -def _legacy_dispatcher(effect, k): - yield Pass(effect, k) - - -class TestWithHandlerDeprecation: - def test_shim_emits_deprecation_warning_for_new_style(self): - fn = _new_style_handler() - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always", DeprecationWarning) - WithHandler(fn, Pure(1)) - dep = [x for x in w if issubclass(x.category, DeprecationWarning)] - assert dep, "expected DeprecationWarning" - msg = str(dep[0].message) - assert "WithHandler" in msg - # The warning should show the recommended replacement. - assert "h(body)" in msg or "h(program)" in msg - - def test_shim_emits_deprecation_warning_for_legacy_dispatcher(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always", DeprecationWarning) - WithHandler(_legacy_dispatcher, Pure(1)) - dep = [x for x in w if issubclass(x.category, DeprecationWarning)] - assert dep, "expected DeprecationWarning" - msg = str(dep[0].message) - assert "WithHandler" in msg - # Legacy path gets a second pointer to defhandler. - assert "defhandler" in msg - - -class TestShimStillWorks: - """Deprecation is soft — calls continue to succeed.""" - - def test_new_style_routes_to_handler_call(self): - fn = _new_style_handler() - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - result = WithHandler(fn, Pure(42)) - # The result should be whatever fn(Pure(42)) returned, which in - # our stub is a _WithHandlerNode. - assert isinstance(result, _WithHandlerNode) - - def test_legacy_returns_node(self): - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - node = WithHandler(_legacy_dispatcher, Pure(7)) - assert isinstance(node, _WithHandlerNode) - - def test_legacy_full_run_still_works(self): - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - body = WithHandler(_legacy_dispatcher, Pure(123)) - assert run(body) == 123 - - -class TestWithHandlerTypeNotDeprecated: - def test_type_alias_usable_without_warning(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always", DeprecationWarning) - node = WithHandlerType(_legacy_dispatcher, Pure(1)) - dep = [x for x in w if issubclass(x.category, DeprecationWarning)] - assert not dep, "WithHandlerType should not warn" - assert isinstance(node, _WithHandlerNode) diff --git a/tests/test_withhandler_validation.py b/tests/test_withhandler_validation.py index a61da214c..fc894e04a 100644 --- a/tests/test_withhandler_validation.py +++ b/tests/test_withhandler_validation.py @@ -1,8 +1,9 @@ -"""WithHandler must reject non-callable handlers at construction time.""" +"""handler() must reject non-callable raw handlers at construction time.""" import pytest -from doeff import WithHandler, do +from doeff import do +from doeff import handler as make_handler @do @@ -10,35 +11,45 @@ def _body(): return 42 -def test_withhandler_rejects_non_callable_int(): +def test_handler_rejects_non_callable_int(): with pytest.raises(TypeError, match="callable"): - WithHandler(42, _body()) + make_handler(42) -def test_withhandler_rejects_non_callable_string(): +def test_handler_rejects_non_callable_string(): with pytest.raises(TypeError, match="callable"): - WithHandler("hello", _body()) + make_handler("hello") -def test_withhandler_rejects_non_callable_none(): +def test_handler_rejects_non_callable_none(): with pytest.raises(TypeError, match="callable"): - WithHandler(None, _body()) + make_handler(None) -def test_withhandler_accepts_do_handler(): +def test_handler_accepts_do_handler(): @do - def handler(effect, k): + def raw_handler(effect, k): from doeff import Pass - yield Pass() + yield Pass(effect, k) - ctrl = WithHandler(handler, _body()) - assert ctrl.handler is handler + ctrl = make_handler(raw_handler)(_body()) + assert ctrl.handler is raw_handler -def test_withhandler_accepts_plain_callable(): - def handler(effect, k): +def test_handler_accepts_plain_callable(): + def raw_handler(effect, k): from doeff import Resume return Resume(k, "ok") - ctrl = WithHandler(handler, _body()) - assert ctrl.handler is handler + ctrl = make_handler(raw_handler)(_body()) + assert ctrl.handler is raw_handler + + +def test_handler_is_idempotent_for_installed_handler(): + @do + def raw_handler(effect, k): + from doeff import Pass + yield Pass(effect, k) + + installed = make_handler(raw_handler) + assert make_handler(installed) is installed