From bd536250be18771a94429cd574296c2809b32967 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:50 +0900 Subject: [PATCH 01/35] Clean Ruff violations in core --- doeff/__init__.py | 116 +++++++++++------- doeff/cli/discovery.py | 6 +- doeff/cli/hy_runner.py | 1 - doeff/cli/run_services.py | 17 ++- doeff/mcp.py | 5 +- doeff/program.py | 37 +++--- doeff/result.py | 5 +- doeff/run.py | 13 +- doeff/traceback.py | 16 +-- tests/_run_helpers.py | 4 +- tests/cli/cli_assets.py | 2 +- tests/cli/test_cli_main.py | 18 ++- tests/cli/test_cli_run.py | 1 - tests/cli/test_doeff_run_context.py | 1 - tests/conftest.py | 15 ++- tests/core/test_call_doexpr_evaluation.py | 7 +- tests/core/test_delegate_non_terminal.py | 2 +- tests/core/test_do_methods.py | 5 +- tests/core/test_doeff_markers.py | 2 +- tests/core/test_doeff_trace_stderr.py | 11 -- tests/core/test_doeff_vm_import_required.py | 3 +- tests/core/test_doexpr_hierarchy.py | 6 +- .../core/test_get_execution_context_effect.py | 20 ++- tests/core/test_gh298_pyvm_consolidation.py | 1 - tests/core/test_import_state_mutation.py | 2 +- tests/core/test_kleisli_characterization.py | 4 +- .../test_map_flatmap_callback_metadata.py | 6 - tests/core/test_nested_do_spawn_gather.py | 15 ++- tests/core/test_pinjected_removal.py | 1 - tests/core/test_program_trace_removed.py | 5 - tests/core/test_runtime_regressions_manual.py | 8 +- tests/core/test_sa001_spec_gaps.py | 11 +- tests/core/test_sa002_spec_gaps.py | 5 +- tests/core/test_sa007_spec_gaps.py | 17 +-- tests/core/test_sa008_correctness_p0.py | 1 - tests/core/test_sa008_runtime_contracts.py | 15 +-- .../test_sa008_runtime_probes_formalized.py | 6 +- tests/core/test_sa008_spec_gaps.py | 20 ++- .../test_sa009_async_handler_spec_gaps.py | 20 +-- tests/core/test_scheduler_transfer.py | 4 +- .../core/test_spawn_gather_with_intercept.py | 23 ++-- tests/core/test_spawn_try_gather.py | 7 +- tests/core/test_spec_gaps.py | 22 ++-- tests/core/test_spec_trace_001_examples.py | 9 +- tests/core/test_trace_coercion_strict.py | 1 - tests/core/test_unified_traceback_system.py | 10 +- tests/core/test_vm_architecture_ocaml5.py | 5 +- .../test_vm_architecture_ocaml5_source.py | 2 - tests/core/test_vm_core_handle_indirection.py | 5 +- tests/core/test_vm_fiber_ownership_g1.py | 3 +- .../test_vm_move_semantics_regressions.py | 2 +- tests/core/test_vm_mutable_state_audit_g0.py | 3 +- tests/core/test_vm_ocaml5_violations.py | 2 +- .../test_await_asyncio_semaphore_deadlock.py | 4 +- tests/effects/test_compat_cleanup_spec.py | 6 - tests/effects/test_control_effects.py | 10 +- tests/effects/test_effect_combinations.py | 4 +- tests/effects/test_env_var_ask.py | 5 +- tests/effects/test_external_promise.py | 5 +- tests/effects/test_lazy_ask.py | 14 ++- .../effects/test_lazy_ask_env_var_fallback.py | 6 +- ...test_lazy_ask_inner_handler_propagation.py | 6 +- tests/effects/test_lazy_ask_semaphore.py | 4 - tests/effects/test_pure_effect.py | 2 +- tests/effects/test_semaphore.py | 3 +- tests/effects/test_spawn_priority.py | 2 - .../test_sync_await_spawn_gather_deadlock.py | 4 +- tests/effects/test_traverse.py | 29 +++-- tests/fixtures_discovery/myapp/__init__.py | 4 +- .../myapp/features/__init__.py | 1 - .../myapp/features/auth/__init__.py | 4 +- tests/misc/test_reader_hashable_keys.py | 2 - tests/public_api/test_api_cleanup.py | 3 +- tests/public_api/test_do_bang_defp_defpp.py | 7 +- .../test_doeff13_hang_regression.py | 3 +- tests/public_api/test_kpc_macro_expansion.py | 5 +- .../test_kpc_macro_runtime_contract.py | 3 +- tests/public_api/test_mcp_tool_def.py | 8 +- tests/public_api/test_mcp_tool_macro.py | 6 +- .../public_api/test_types_001_composition.py | 5 +- .../test_types_001_handler_protocol.py | 6 +- tests/public_api/test_types_001_validation.py | 1 - tests/test_architecture_violations.py | 31 ++--- tests/test_await_concurrency.py | 11 +- tests/test_await_http_memory.py | 12 +- tests/test_cache_memory_bisect.py | 14 +-- tests/test_cli_env_merge_with_program.py | 3 +- tests/test_core_effects.py | 23 ++-- tests/test_deep_handler.py | 3 +- tests/test_deftest_macro.py | 15 +-- tests/test_do_bang_setv.py | 4 +- tests/test_error_propagation.py | 8 +- tests/test_error_traceback_consistency.py | 5 +- tests/test_execution_scope.py | 3 +- tests/test_finally_doctrl.py | 2 +- tests/test_get_handlers_defk.py | 24 ++-- tests/test_handle_macro.py | 17 ++- tests/test_handler_chain_traversal.py | 7 +- tests/test_handler_exception_catchable.py | 6 +- tests/test_handler_nested_do.py | 5 +- tests/test_intercept.py | 8 +- tests/test_llm_multi_provider_handlers.py | 4 +- tests/test_macro_self_contained.py | 10 +- .../test_memo_rewriter_spawn_continuation.py | 29 ++--- tests/test_new_vm.py | 12 +- .../test_pass_dispatch_context_regression.py | 9 +- tests/test_pass_then_typed_handler.py | 12 +- tests/test_pass_traceback.py | 6 +- tests/test_pyclass_pickle.py | 1 - tests/test_run_program_services.py | 3 - tests/test_scheduled_handler_isolation.py | 9 +- tests/test_scheduler.py | 14 +-- tests/test_segment_owned_state.py | 6 +- tests/test_spawn_with_handlers.py | 12 +- tests/test_traceback_dedup_frames.py | 12 +- tests/test_try_deep_in_handler_body.py | 11 +- tests/test_try_handler.py | 5 +- tests/test_try_handler_scope.py | 5 +- tests/test_try_in_handler_body.py | 5 +- ...est_with_handler_type_filter_regression.py | 3 +- tests/test_with_observe_visibility.py | 19 +-- tests/test_withhandler_shim_deprecation.py | 4 +- 122 files changed, 492 insertions(+), 610 deletions(-) diff --git a/doeff/__init__.py b/doeff/__init__.py index 9f453ba02..0c1bbdad5 100644 --- a/doeff/__init__.py +++ b/doeff/__init__.py @@ -4,31 +4,78 @@ Backed by a Rust VM with OCaml 5-aligned effect handler architecture. """ +# ruff: noqa: I001 - import order avoids doeff_core_effects circular imports. from collections.abc import Generator -from typing import Any, Callable, ParamSpec, TypeVar - -from doeff.do import do -from doeff.program import ( - Apply, - Expand, - GetExecutionContext, - GetHandlers, - GetOuterHandlers, - GetTraceback, - Pass, - Perform, - Pure, - Resume, - Transfer, - WithHandler, - WithHandlerType, - WithObserve as WithObserveRaw, - program, -) -from doeff_vm import Callable as _VmCallable +from typing import Any - -def WithObserve(observer, body): +from doeff_vm import Callable as Callable +from doeff_vm import Callable as _VmCallable +from doeff_vm import EffectBase +from doeff_vm import K as K +from doeff_vm import PyVM as PyVM +from doeff_vm import UnhandledEffect as UnhandledEffect + +from doeff.cli.run_services import DoeffRunContext as DoeffRunContext +from doeff.do import do as do +from doeff.mcp import McpParamSchema as McpParamSchema +from doeff.mcp import McpToolDef as McpToolDef +from doeff.program import Apply as Apply +from doeff.program import Expand as Expand +from doeff.program import GetExecutionContext as GetExecutionContext +from doeff.program import GetHandlers as GetHandlers +from doeff.program import GetOuterHandlers as GetOuterHandlers +from doeff.program import GetTraceback as GetTraceback +from doeff.program import Pass as Pass +from doeff.program import Perform as Perform +from doeff.program import Pure as Pure +from doeff.program import Resume as Resume +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 WithObserve as WithObserveRaw +from doeff.program import program as program +from doeff.result import Err as Err +from doeff.result import Maybe as Maybe +from doeff.result import Nothing as Nothing +from doeff.result import Ok as Ok +from doeff.result import Some as Some +from doeff.run import run as run + +from doeff_core_effects.effects import Ask as Ask +from doeff_core_effects.effects import Await as Await +from doeff_core_effects.effects import Get as Get +from doeff_core_effects.effects import Listen as Listen +from doeff_core_effects.effects import Local as Local +from doeff_core_effects.effects import Put as Put +from doeff_core_effects.effects import Slog as Slog +from doeff_core_effects.effects import Tell as Tell +from doeff_core_effects.effects import Try as Try +from doeff_core_effects.effects import WriterTellEffect as WriterTellEffect +from doeff_core_effects.effects import slog as slog +from doeff_core_effects.scheduler import PRIORITY_HIGH as PRIORITY_HIGH +from doeff_core_effects.scheduler import PRIORITY_IDLE as PRIORITY_IDLE +from doeff_core_effects.scheduler import PRIORITY_NORMAL as PRIORITY_NORMAL +from doeff_core_effects.scheduler import AcquireSemaphore as AcquireSemaphore +from doeff_core_effects.scheduler import Cancel as Cancel +from doeff_core_effects.scheduler import CompletePromise as CompletePromise +from doeff_core_effects.scheduler import CreateExternalPromise as CreateExternalPromise +from doeff_core_effects.scheduler import CreatePromise as CreatePromise +from doeff_core_effects.scheduler import CreateSemaphore as CreateSemaphore +from doeff_core_effects.scheduler import FailPromise as FailPromise +from doeff_core_effects.scheduler import Future as Future +from doeff_core_effects.scheduler import Gather as Gather +from doeff_core_effects.scheduler import Promise as Promise +from doeff_core_effects.scheduler import Race as Race +from doeff_core_effects.scheduler import ReleaseSemaphore as ReleaseSemaphore +from doeff_core_effects.scheduler import Semaphore as Semaphore +from doeff_core_effects.scheduler import Spawn as Spawn +from doeff_core_effects.scheduler import Task as Task +from doeff_core_effects.scheduler import Wait as Wait + + +def WithObserve(observer, body): # noqa: N802 - public compatibility constructor """Install observer and run body under it. Accepts a plain Python callable as observer — automatically wraps it @@ -43,30 +90,9 @@ def WithObserve(observer, body): f"WithObserve: observer must be callable, got {type(observer).__name__}" ) return WithObserveRaw(_VmCallable(observer), body) -from doeff.program import ResumeThrow, TransferThrow -from doeff.run import run -from doeff.result import Ok, Err, Some, Nothing, Maybe # noqa: F811 -from doeff.mcp import McpToolDef, McpParamSchema -from doeff_vm import Callable, EffectBase, K, PyVM, UnhandledEffect Effect = EffectBase -# --- Compat re-exports (old API names) --- -# Effects (now in doeff_core_effects.effects) -from doeff_core_effects.effects import ( # noqa: E402 - Ask, Get, Put, Local, Listen, Await, Try, - WriterTellEffect, Slog, slog, Tell, -) -# Scheduler effects (now in doeff_core_effects.scheduler) -from doeff_core_effects.scheduler import ( # noqa: E402 - Spawn, Gather, Wait, Race, Cancel, - CreatePromise, CompletePromise, FailPromise, - CreateSemaphore, AcquireSemaphore, ReleaseSemaphore, - CreateExternalPromise, - Task, Future, Promise, Semaphore, - PRIORITY_IDLE, PRIORITY_NORMAL, PRIORITY_HIGH, -) - # DoExpr — virtual base type for all program nodes. # Enables isinstance(x, DoExpr) to check if a value is any program node. _DOEXPR_TYPES = ( @@ -151,6 +177,4 @@ def __getattr__(self, attr): async_run = _Removed("async_run", "use run() with scheduled()") default_async_handlers = _Removed("default_async_handlers", "compose handlers explicitly with WithHandler") -from doeff.cli.run_services import DoeffRunContext - __version__ = "0.4.1" diff --git a/doeff/cli/discovery.py b/doeff/cli/discovery.py index 5069126ac..db3eb0015 100644 --- a/doeff/cli/discovery.py +++ b/doeff/cli/discovery.py @@ -7,7 +7,7 @@ import importlib import inspect import logging -from typing import Any, Protocol +from typing import Any, Protocol, cast from doeff.cli.profiling import profile @@ -54,8 +54,8 @@ class IndexerBasedDiscovery: def __init__(self, symbol_loader: SymbolLoader | None = None): with profile("Import doeff_indexer", indent=1): try: - from doeff_indexer import Indexer - self.indexer_class = Indexer + indexer_module = cast(Any, importlib.import_module("doeff_indexer")) + self.indexer_class = indexer_module.Indexer except ImportError as e: raise ImportError( "doeff-indexer not found. Install with: pip install doeff-indexer" diff --git a/doeff/cli/hy_runner.py b/doeff/cli/hy_runner.py index 9dfc46cd5..815927345 100644 --- a/doeff/cli/hy_runner.py +++ b/doeff/cli/hy_runner.py @@ -16,7 +16,6 @@ from dataclasses import dataclass from typing import Any - _REQUIRE_PRELUDE = ( "(require doeff-hy.macros [do! <-])\n" "(require doeff-hy.handle [defhandler handle])" diff --git a/doeff/cli/run_services.py b/doeff/cli/run_services.py index 56aa569a2..d7fe61244 100644 --- a/doeff/cli/run_services.py +++ b/doeff/cli/run_services.py @@ -1,5 +1,6 @@ """Run services — symbol resolution, interpreter discovery, program execution.""" +import contextlib import importlib import importlib.util import sys @@ -12,10 +13,8 @@ # Without this, importlib.import_module for a .hy module silently creates an # empty entry in sys.modules, and subsequent getattr(mod, name) fails with # "module '' has no attribute ''". -try: +with contextlib.suppress(ImportError): import hy.importer # noqa: F401 — registers sys.meta_path hook -except ImportError: - pass from doeff.cli.profiling import profile @@ -173,13 +172,19 @@ def resolve_context(ctx: RunContext) -> ResolvedRunContext: def default_interpreter(program: Any) -> Any: """Default interpreter: run with standard handlers + scheduler.""" - from doeff import run, WithHandler from doeff_core_effects.handlers import ( - lazy_ask, state, writer, try_handler, slog_handler, - listen_handler, await_handler, + await_handler, + lazy_ask, + listen_handler, + slog_handler, + state, + try_handler, + writer, ) from doeff_core_effects.scheduler import scheduled + from doeff import WithHandler, run + handlers = [ lazy_ask(), state(), writer(), try_handler, slog_handler(), listen_handler, await_handler(), diff --git a/doeff/mcp.py b/doeff/mcp.py index 04ae6775f..11c4ca4db 100644 --- a/doeff/mcp.py +++ b/doeff/mcp.py @@ -32,8 +32,9 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from doeff.program import Expand @@ -82,7 +83,7 @@ class McpToolDef: name: str description: str params: tuple[McpParamSchema, ...] - handler: Callable[..., "Expand"] + handler: Callable[..., Expand] def input_schema(self) -> dict[str, Any]: """Generate the ``inputSchema`` object for the MCP ``tools/list`` response.""" diff --git a/doeff/program.py b/doeff/program.py index aef4f7902..a66e55492 100644 --- a/doeff/program.py +++ b/doeff/program.py @@ -4,23 +4,21 @@ The VM classifies them via downcast (not tag-based getattr). """ -from doeff_vm import ( - Pure, - Perform, - Resume, - Transfer, - Apply, - Expand, - Pass, - WithHandler as _WithHandlerNode, - ResumeThrow, - TransferThrow, - WithObserve, - GetTraceback, - GetExecutionContext, - GetHandlers, - GetOuterHandlers, -) +from doeff_vm import Apply as Apply +from doeff_vm import Expand as Expand +from doeff_vm import GetExecutionContext as GetExecutionContext +from doeff_vm import GetHandlers as GetHandlers +from doeff_vm import GetOuterHandlers as GetOuterHandlers +from doeff_vm import GetTraceback as GetTraceback +from doeff_vm import Pass as Pass +from doeff_vm import Perform as Perform +from doeff_vm import Pure as Pure +from doeff_vm import Resume as Resume +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 WithObserve as WithObserve WithHandlerType = _WithHandlerNode @@ -41,7 +39,7 @@ ) -def WithHandler(h, body, *args, **kwargs): +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 → @@ -78,7 +76,8 @@ def program(gen_fn, *args): The factory calls gen_fn and wraps the generator as IRStream explicitly. """ - from doeff_vm import Callable as VmCallable, IRStream + from doeff_vm import Callable as VmCallable + from doeff_vm import IRStream def factory(*inner_args): gen = gen_fn(*inner_args) diff --git a/doeff/result.py b/doeff/result.py index 045d41998..4b7d08b21 100644 --- a/doeff/result.py +++ b/doeff/result.py @@ -16,7 +16,7 @@ case Nothing: ... """ -from doeff_vm import Ok, Err # noqa: F401 — re-export Rust types +from doeff_vm import Err, Ok # noqa: F401 — re-export Rust types class Some: @@ -91,5 +91,4 @@ def _get_nothing(): # Maybe is the union type (for type hints) -from typing import Union -Maybe = Union[Some, _NothingType] +Maybe = Some | _NothingType diff --git a/doeff/run.py b/doeff/run.py index f0033e3c9..dc440e223 100644 --- a/doeff/run.py +++ b/doeff/run.py @@ -24,7 +24,7 @@ def run(doexpr): def _enrich_and_print(exception): """Enrich exception with doeff traceback and print to stderr.""" try: - if hasattr(exception, '__doeff_traceback__'): + if hasattr(exception, "__doeff_traceback__"): # VM already set __doeff_traceback__ (e.g. handler chain), # but pure Python frames from __traceback__ may be missing. _merge_python_frames(exception) @@ -38,7 +38,7 @@ def _enrich_and_print(exception): pass # don't mask the original error -def _merge_python_frames(exc): +def _merge_python_frames(exc): # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged """Merge pure Python __traceback__ frames into existing __doeff_traceback__. When the Rust VM sets __doeff_traceback__ (e.g. handler chain entries), @@ -52,8 +52,8 @@ def _merge_python_frames(exc): py_frames = [] for fs in tb_mod.extract_tb(tb): fn = fs.filename - if any(p in fn for p in ('/doeff_vm/', '/doeff/do.py', '/doeff/run.py', - '/doeff_core_effects/')): + if any(p in fn for p in ("/doeff_vm/", "/doeff/do.py", "/doeff/run.py", + "/doeff_core_effects/")): continue py_frames.append([fs.name, fs.filename, fs.lineno]) if not py_frames: @@ -91,10 +91,9 @@ def _enrich_exception_traceback(exc): frames = [] for fs in tb_mod.extract_tb(tb): fn = fs.filename - name = fs.name # Skip doeff VM/framework internals - if any(p in fn for p in ('/doeff_vm/', '/doeff/do.py', '/doeff/run.py', - '/doeff_core_effects/')): + if any(p in fn for p in ("/doeff_vm/", "/doeff/do.py", "/doeff/run.py", + "/doeff_core_effects/")): continue frames.append([fs.name, fs.filename, fs.lineno]) if frames: diff --git a/doeff/traceback.py b/doeff/traceback.py index 5e5f33d09..891a0abe6 100644 --- a/doeff/traceback.py +++ b/doeff/traceback.py @@ -13,12 +13,12 @@ import linecache import os -_INTERNAL_PATHS = ('/doeff_core_effects/', '/doeff/do.py', '/doeff/run.py', '/doeff_vm/') +_INTERNAL_PATHS = ("/doeff_core_effects/", "/doeff/do.py", "/doeff/run.py", "/doeff_vm/") def format_default(exception): """Render doeff traceback from __doeff_traceback__.""" - tb_data = getattr(exception, '__doeff_traceback__', None) + tb_data = getattr(exception, "__doeff_traceback__", None) if not tb_data: return None @@ -69,7 +69,7 @@ def _render_frame(func_name, source_file, source_line, count=1): source_text = _get_source_line(source_file, source_line) result = f"\n {func_name}() {short_file}:{source_line}" if count > 1: - result += f" [×{count}]" + result += f" [×{count}]" # noqa: RUF001 - traceback format intentionally uses multiplication sign. if source_text: result += f"\n {source_text}" return result @@ -78,11 +78,11 @@ def _render_frame(func_name, source_file, source_line, count=1): def _render_handler_chain(handler_chain): names = [] for name in handler_chain: - name = str(name) - if ".." in name: - name = name.split("..")[0] - if not names or names[-1] != name: - names.append(name) + rendered_name = str(name) + if ".." in rendered_name: + rendered_name = rendered_name.split("..")[0] + if not names or names[-1] != rendered_name: + names.append(rendered_name) if not names: return "" lines = ["\n handlers:"] diff --git a/tests/_run_helpers.py b/tests/_run_helpers.py index 38969ddb6..199ecb929 100644 --- a/tests/_run_helpers.py +++ b/tests/_run_helpers.py @@ -13,7 +13,6 @@ from typing import Any -from doeff import Err, Ok, WithHandler, run as _run from doeff_core_effects.handlers import ( await_handler, lazy_ask, @@ -27,6 +26,9 @@ ) from doeff_core_effects.scheduler import scheduled +from doeff import Err, Ok, WithHandler +from doeff import run as _run + def default_handlers(env: Any = None, store: Any = None) -> list[Any]: """Return the standard handler chain used in legacy tests. diff --git a/tests/cli/cli_assets.py b/tests/cli/cli_assets.py index 4d0537aae..caf4c1a17 100644 --- a/tests/cli/cli_assets.py +++ b/tests/cli/cli_assets.py @@ -1,6 +1,6 @@ from __future__ import annotations -from doeff import Ask, Program, default_handlers, do, run +from doeff import Ask, Program, do from tests._run_helpers import run_with_defaults sample_program: Program[int] = Program.pure(5) diff --git a/tests/cli/test_cli_main.py b/tests/cli/test_cli_main.py index e8649382a..24d04d045 100644 --- a/tests/cli/test_cli_main.py +++ b/tests/cli/test_cli_main.py @@ -5,10 +5,9 @@ import json import pytest -from doeff import Program, run from doeff import __main__ as cli -from tests._run_helpers import run_with_defaults +from doeff import run pytestmark = pytest.mark.cli @@ -59,17 +58,24 @@ def test_parse_set_vars_empty_key() -> None: def _env_interpreter(program, env=None): """Interpreter for tests that resolves Ask from env dict.""" - from doeff import WithHandler from doeff_core_effects.handlers import ( - reader, state, writer, try_handler, slog_handler, - local_handler, listen_handler, await_handler, + await_handler, + listen_handler, + local_handler, + reader, + slog_handler, + state, + try_handler, + writer, ) from doeff_core_effects.scheduler import scheduled + from doeff import WithHandler + env_dict = {} if env is not None: result = run(env) - env_dict = result.value if hasattr(result, 'value') else result + env_dict = result.value if hasattr(result, "value") else result handlers = [ reader(env_dict), state(), writer(), try_handler, slog_handler(), diff --git a/tests/cli/test_cli_run.py b/tests/cli/test_cli_run.py index 245d61442..117e221c8 100644 --- a/tests/cli/test_cli_run.py +++ b/tests/cli/test_cli_run.py @@ -6,7 +6,6 @@ from pathlib import Path import pytest -from tests._run_helpers import run_with_defaults pytestmark = [ pytest.mark.cli, diff --git a/tests/cli/test_doeff_run_context.py b/tests/cli/test_doeff_run_context.py index f5d82b237..c19ce77d7 100644 --- a/tests/cli/test_doeff_run_context.py +++ b/tests/cli/test_doeff_run_context.py @@ -2,7 +2,6 @@ from __future__ import annotations -import json import os import subprocess from pathlib import Path diff --git a/tests/conftest.py b/tests/conftest.py index 309504386..059b3c6f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,14 +11,21 @@ from typing import Any, Literal, Protocol, TypeVar import pytest - -from doeff import WithHandler, do, run from doeff_core_effects.handlers import ( - reader, state, writer, try_handler, slog_handler, - local_handler, listen_handler, await_handler, lazy_ask, + await_handler, + lazy_ask, + listen_handler, + local_handler, + reader, + slog_handler, + state, + try_handler, + writer, ) from doeff_core_effects.scheduler import scheduled +from doeff import WithHandler, run + T = TypeVar("T") RunnerMode = Literal["sync"] diff --git a/tests/core/test_call_doexpr_evaluation.py b/tests/core/test_call_doexpr_evaluation.py index 37162830e..147462f44 100644 --- a/tests/core/test_call_doexpr_evaluation.py +++ b/tests/core/test_call_doexpr_evaluation.py @@ -1,9 +1,8 @@ from __future__ import annotations -import doeff_vm - -from doeff import Apply, Ask, Effect, Get, KleisliProgram, Program, Pure, default_handlers, do, run -from doeff import ProgramBase +from doeff import ( + do, +) from tests._run_helpers import run_with_defaults diff --git a/tests/core/test_delegate_non_terminal.py b/tests/core/test_delegate_non_terminal.py index 134b6a8d9..cb72875c9 100644 --- a/tests/core/test_delegate_non_terminal.py +++ b/tests/core/test_delegate_non_terminal.py @@ -1,6 +1,5 @@ from __future__ import annotations -from tests._run_helpers import run_with_defaults from doeff import ( Ask, AskEffect, @@ -11,6 +10,7 @@ WithHandler, do, ) +from tests._run_helpers import run_with_defaults class _ProbeEffect(EffectBase): diff --git a/tests/core/test_do_methods.py b/tests/core/test_do_methods.py index 8eb33d4bd..48ae9c83f 100644 --- a/tests/core/test_do_methods.py +++ b/tests/core/test_do_methods.py @@ -3,10 +3,7 @@ import inspect from typing import Any -import doeff_vm -import pytest - -from doeff import Program, default_handlers, do, run +from doeff import Program, do from tests._run_helpers import run_with_defaults diff --git a/tests/core/test_doeff_markers.py b/tests/core/test_doeff_markers.py index b1eebe42a..7d5bbcaa3 100644 --- a/tests/core/test_doeff_markers.py +++ b/tests/core/test_doeff_markers.py @@ -8,7 +8,7 @@ def basic_interpreter(program: Program): # doeff: interpreter def advanced_interpreter( # doeff: interpreter program: Program, - config: dict = None + config: dict | None = None ): """An interpreter with marker on function definition line.""" diff --git a/tests/core/test_doeff_trace_stderr.py b/tests/core/test_doeff_trace_stderr.py index 0ca362845..491a855d5 100644 --- a/tests/core/test_doeff_trace_stderr.py +++ b/tests/core/test_doeff_trace_stderr.py @@ -1,26 +1,15 @@ from __future__ import annotations -from dataclasses import dataclass - import pytest from doeff import ( Ask, - Delegate, - Effect, - EffectBase, Program, - WithHandler, - async_run, - default_async_handlers, - default_handlers, do, - run, ) from tests._run_helpers import run_with_defaults - def test_run_prints_trace_when_flag_is_true(capsys: pytest.CaptureFixture[str]) -> None: @do def failing() -> Program[int]: diff --git a/tests/core/test_doeff_vm_import_required.py b/tests/core/test_doeff_vm_import_required.py index 99e2e85e5..d711050b3 100644 --- a/tests/core/test_doeff_vm_import_required.py +++ b/tests/core/test_doeff_vm_import_required.py @@ -1,12 +1,10 @@ from __future__ import annotations import json -import re import subprocess import sys from pathlib import Path - ROOT = Path(__file__).resolve().parents[2] @@ -37,6 +35,7 @@ def find_spec(self, fullname, path=None, target=None): cwd=ROOT, capture_output=True, text=True, + check=False, ) assert result.returncode == 0, result.stderr or result.stdout return json.loads(result.stdout) diff --git a/tests/core/test_doexpr_hierarchy.py b/tests/core/test_doexpr_hierarchy.py index c9c876ef6..e81b50467 100644 --- a/tests/core/test_doexpr_hierarchy.py +++ b/tests/core/test_doexpr_hierarchy.py @@ -1,12 +1,8 @@ from __future__ import annotations -import pytest from dataclasses import dataclass -from doeff import Ask, Program, ProgramBase, do -from doeff import EffectBase -from tests._run_helpers import run_with_defaults - +from doeff import Ask, EffectBase def test_effects_do_not_expose_direct_composition_methods() -> None: diff --git a/tests/core/test_get_execution_context_effect.py b/tests/core/test_get_execution_context_effect.py index 9f6182057..1254cddab 100644 --- a/tests/core/test_get_execution_context_effect.py +++ b/tests/core/test_get_execution_context_effect.py @@ -1,27 +1,25 @@ from __future__ import annotations -import asyncio import inspect -import warnings -from dataclasses import dataclass from pathlib import Path from typing import Any -import doeff_vm -import pytest - -from doeff import Await, Effect, Gather, Program, Spawn, async_run, default_async_handlers, do -from doeff import EffectBase -# REMOVED: from doeff import ProgramCallStack -from doeff import Delegate, default_handlers, run from doeff_vm import ( GetExecutionContext, - GetTraceback, Pass, Resume, WithHandler, ) + +# REMOVED: from doeff import ProgramCallStack +from doeff import ( + Delegate, + Effect, + Program, + do, +) from tests._run_helpers import run_with_defaults + # REMOVED: from doeff.traceback import build_doeff_traceback ROOT = Path(__file__).resolve().parents[2] diff --git a/tests/core/test_gh298_pyvm_consolidation.py b/tests/core/test_gh298_pyvm_consolidation.py index b0d766025..439c65358 100644 --- a/tests/core/test_gh298_pyvm_consolidation.py +++ b/tests/core/test_gh298_pyvm_consolidation.py @@ -1,6 +1,5 @@ from pathlib import Path - ROOT = Path(__file__).resolve().parents[2] VM_PYVM = ROOT / "packages" / "doeff-vm" / "src" / "pyvm.rs" VM_CORE_PYVM = ROOT / "packages" / "doeff-vm-core" / "src" / "pyvm.rs" diff --git a/tests/core/test_import_state_mutation.py b/tests/core/test_import_state_mutation.py index 5dcafd91e..77725f0f8 100644 --- a/tests/core/test_import_state_mutation.py +++ b/tests/core/test_import_state_mutation.py @@ -6,7 +6,6 @@ import sys from pathlib import Path - ROOT = Path(__file__).resolve().parents[2] VM_PYTHONPATH = str(ROOT / "packages" / "doeff-vm") @@ -23,6 +22,7 @@ def _run_python(script: str) -> dict[str, object]: env=env, capture_output=True, text=True, + check=False, ) assert result.returncode == 0, result.stderr or result.stdout return json.loads(result.stdout) diff --git a/tests/core/test_kleisli_characterization.py b/tests/core/test_kleisli_characterization.py index 3a6d2071a..e04785324 100644 --- a/tests/core/test_kleisli_characterization.py +++ b/tests/core/test_kleisli_characterization.py @@ -179,7 +179,7 @@ def handler(effect: Effect, _k): if isinstance(effect, Ping): state_val = yield Get("key") return (yield Resume(_k, f"handled:{state_val}")) - yield Pass(effect, k) + yield Pass(effect, k) # noqa: F821 - legacy removed API reference is intentionally preserved def body(): value = yield Ping("hello") @@ -278,7 +278,7 @@ def body(): def test_with_handler_post_composition() -> None: @do def handler(_effect: Effect, _k): - yield Pass(effect, k) + yield Pass(effect, k) # noqa: F821 - legacy removed API reference is intentionally preserved @do def body(): diff --git a/tests/core/test_map_flatmap_callback_metadata.py b/tests/core/test_map_flatmap_callback_metadata.py index 00d1e49c6..11b429432 100644 --- a/tests/core/test_map_flatmap_callback_metadata.py +++ b/tests/core/test_map_flatmap_callback_metadata.py @@ -1,10 +1,4 @@ from __future__ import annotations -import warnings -from typing import Any -import pytest - -from doeff import Program, default_handlers, do, run -from tests._run_helpers import run_with_defaults # REMOVED: from doeff import ProgramCallStack diff --git a/tests/core/test_nested_do_spawn_gather.py b/tests/core/test_nested_do_spawn_gather.py index 167410941..47147223b 100644 --- a/tests/core/test_nested_do_spawn_gather.py +++ b/tests/core/test_nested_do_spawn_gather.py @@ -9,22 +9,22 @@ The wrapper does nothing special — just yields slog() then delegates to async_gather. """ import asyncio -from dataclasses import dataclass, field -from typing import Any, List +from dataclasses import dataclass +from typing import Any -import pytest +from doeff_core_effects.effects import EffectBase from doeff import ( + AcquireSemaphore, Ask, Await, CreateSemaphore, - AcquireSemaphore, Effect, - ReleaseSemaphore, EffectGenerator, Gather, Local, Pass, + ReleaseSemaphore, Resume, Spawn, Try, @@ -32,7 +32,6 @@ do, slog, ) -from doeff_core_effects.effects import EffectBase from tests._run_helpers import run_with_defaults ProgramLike = Any # removed shim @@ -105,7 +104,7 @@ def _g() -> EffectGenerator[list]: @do def throttled_gather( *programs: ProgramLike, concurrency: int -) -> EffectGenerator[List[Any]]: +) -> EffectGenerator[list[Any]]: """Wrapper that adds slog before delegating to async_gather.""" semaphore = yield CreateSemaphore(concurrency) yield slog(msg="gathering", level="debug", concurrency=concurrency, total=len(programs)) @@ -118,7 +117,7 @@ def throttled_gather( @do def throttled_gather_with_progress( *programs: ProgramLike, concurrency: int, description: str = "" -) -> EffectGenerator[List[Any]]: +) -> EffectGenerator[list[Any]]: """Another wrapper layer (like the real code).""" if description: yield slog(msg=description, level="info") diff --git a/tests/core/test_pinjected_removal.py b/tests/core/test_pinjected_removal.py index d451dde37..40c89636f 100644 --- a/tests/core/test_pinjected_removal.py +++ b/tests/core/test_pinjected_removal.py @@ -2,7 +2,6 @@ from pathlib import Path - ROOT = Path(__file__).resolve().parents[2] diff --git a/tests/core/test_program_trace_removed.py b/tests/core/test_program_trace_removed.py index 40fe93377..258803940 100644 --- a/tests/core/test_program_trace_removed.py +++ b/tests/core/test_program_trace_removed.py @@ -1,12 +1,7 @@ from __future__ import annotations -from importlib import import_module, util - import doeff_vm -import doeff - - def test_get_trace_doctrl_removed() -> None: assert not hasattr(doeff_vm, "ProgramTraceEffect") diff --git a/tests/core/test_runtime_regressions_manual.py b/tests/core/test_runtime_regressions_manual.py index 040d82631..3e4e73f41 100644 --- a/tests/core/test_runtime_regressions_manual.py +++ b/tests/core/test_runtime_regressions_manual.py @@ -1,18 +1,15 @@ from __future__ import annotations import doeff_vm -import pytest from doeff import ( Ask, Effect, EffectBase, - Err, Gather, Get, Local, MissingEnvKeyError, - Ok, Pass, Program, Put, @@ -21,11 +18,8 @@ Try, WithHandler, do, - run, ) -from doeff_core_effects.handlers import state as state_handler -from doeff_core_effects.scheduler import TaskCompleted -from tests._run_helpers import default_handlers, run_with_defaults, wrap_with_defaults +from tests._run_helpers import run_with_defaults def _rust_ok_err_classes() -> tuple[type, type]: diff --git a/tests/core/test_sa001_spec_gaps.py b/tests/core/test_sa001_spec_gaps.py index 0bc2c1438..5c57e27a7 100644 --- a/tests/core/test_sa001_spec_gaps.py +++ b/tests/core/test_sa001_spec_gaps.py @@ -14,14 +14,9 @@ from __future__ import annotations -import inspect import pathlib import re -import pytest - -from doeff import Get, Put, default_handlers, run -from tests._run_helpers import run_with_defaults # REMOVED: from doeff.program import GeneratorProgram RUST_SRC = pathlib.Path(__file__).resolve().parents[2] / "packages" / "doeff-vm" / "src" @@ -30,7 +25,7 @@ def _prog(gen_factory): """Wrap a generator factory into a GeneratorProgram (has to_generator).""" - return GeneratorProgram(gen_factory) + return GeneratorProgram(gen_factory) # noqa: F821 - legacy removed API reference is intentionally preserved def _read_rust(filename: str) -> str: @@ -93,6 +88,7 @@ class TestSA001G07BasesWired: def test_python_effectbase_extends_rust(self): """Python EffectBase must be isinstance-compatible with Rust PyEffectBase.""" from doeff_vm import EffectBase as RustEffectBase + from doeff import EffectBase assert issubclass(EffectBase, RustEffectBase), ( @@ -141,8 +137,7 @@ class TestSA001G12BaseClasses: def test_do_call_result_is_not_effectbase(self): """@do call results should be DoCtrl, not EffectBase values.""" - from doeff import do - from doeff import EffectBase + from doeff import EffectBase, do @do def identity(x: int): diff --git a/tests/core/test_sa002_spec_gaps.py b/tests/core/test_sa002_spec_gaps.py index 449c8936b..16fa33d3d 100644 --- a/tests/core/test_sa002_spec_gaps.py +++ b/tests/core/test_sa002_spec_gaps.py @@ -5,9 +5,8 @@ from __future__ import annotations -from pathlib import Path import re - +from pathlib import Path ROOT = Path(__file__).resolve().parents[2] RUST_SRC = ROOT / "packages" / "doeff-vm" / "src" @@ -48,7 +47,7 @@ def _extract_fn_body(source: str, fn_name: str) -> str: -def test_SA_002_G03_no_implicit_kpc_install_in_vm_new() -> None: +def test_SA_002_G03_no_implicit_kpc_install_in_vm_new() -> None: # noqa: N802 - public or spec test name is intentionally stable src = _read(RUST_SRC / "pyvm.rs") body = _extract_fn_body(src, "new") assert "KpcHandlerFactory" not in body diff --git a/tests/core/test_sa007_spec_gaps.py b/tests/core/test_sa007_spec_gaps.py index 283bd3e9b..a546a242e 100644 --- a/tests/core/test_sa007_spec_gaps.py +++ b/tests/core/test_sa007_spec_gaps.py @@ -9,7 +9,8 @@ import pytest -from doeff import Ask, Delegate, Effect, Resume, WithHandler, default_handlers, do, run +from doeff import Ask, Resume + # REMOVED: from doeff.program import GeneratorProgram @@ -26,32 +27,32 @@ def _read(path: Path) -> str: return path.read_text(encoding="utf-8") -def _mk_program(value: int = 42) -> GeneratorProgram[int]: +def _mk_program(value: int = 42) -> GeneratorProgram[int]: # noqa: F821 - legacy removed API reference is intentionally preserved def gen(): return value - yield # noqa: RET504 + yield - return GeneratorProgram(gen) + return GeneratorProgram(gen) # noqa: F821 - legacy removed API reference is intentionally preserved -def test_SA_007_G04_dothunk_removed_from_public_hierarchy() -> None: +def test_SA_007_G04_dothunk_removed_from_public_hierarchy() -> None: # noqa: N802 - public or spec test name is intentionally stable src = _read(ROOT / "doeff" / "program.py") assert "class DoThunk" not in src -def test_SA_007_G07_get_handlers_preserves_identity_not_placeholder() -> None: +def test_SA_007_G07_get_handlers_preserves_identity_not_placeholder() -> None: # noqa: N802 - public or spec test name is intentionally stable value_src = _read(ROOT / "packages" / "doeff-vm" / "src" / "value.rs") cont_src = _read(ROOT / "packages" / "doeff-vm" / "src" / "continuation.rs") assert '"rust_program_handler"' not in value_src assert '"rust_program_handler"' not in cont_src -def test_SA_007_G08_classifier_avoids_concrete_doctrl_type_checks() -> None: +def test_SA_007_G08_classifier_avoids_concrete_doctrl_type_checks() -> None: # noqa: N802 - public or spec test name is intentionally stable src = _read(ROOT / "packages" / "doeff-vm" / "src" / "pyvm.rs") assert "is_instance_of::" not in src assert "is_instance_of::" not in src @@ -59,6 +60,6 @@ def test_SA_007_G08_classifier_avoids_concrete_doctrl_type_checks() -> None: assert "is_instance_of::" not in src -def test_SA_007_G03_resume_constructor_validates_k_handle() -> None: +def test_SA_007_G03_resume_constructor_validates_k_handle() -> None: # noqa: N802 - public or spec test name is intentionally stable with pytest.raises(TypeError, match=r"(?i)k"): Resume("not_k", Ask("x")) diff --git a/tests/core/test_sa008_correctness_p0.py b/tests/core/test_sa008_correctness_p0.py index 2f088c0e7..f18d9c5e5 100644 --- a/tests/core/test_sa008_correctness_p0.py +++ b/tests/core/test_sa008_correctness_p0.py @@ -4,7 +4,6 @@ from pathlib import Path - ROOT = Path(__file__).resolve().parents[2] diff --git a/tests/core/test_sa008_runtime_contracts.py b/tests/core/test_sa008_runtime_contracts.py index 2c05a376d..d616a1e73 100644 --- a/tests/core/test_sa008_runtime_contracts.py +++ b/tests/core/test_sa008_runtime_contracts.py @@ -7,20 +7,13 @@ import asyncio -import pytest - -from doeff import Await, Gather, Modify, Spawn, async_run, default_async_handlers, default_handlers, do, run -from doeff_core_effects.effects import Get, Put +from doeff import ( + Await, + do, +) from tests._run_helpers import run_with_defaults - - - - - - - def test_sa008_sync_await_runs_in_default_handlers() -> None: @do def prog(): diff --git a/tests/core/test_sa008_runtime_probes_formalized.py b/tests/core/test_sa008_runtime_probes_formalized.py index 6008ebf38..f443e1d7c 100644 --- a/tests/core/test_sa008_runtime_probes_formalized.py +++ b/tests/core/test_sa008_runtime_probes_formalized.py @@ -5,14 +5,10 @@ from __future__ import annotations -import doeff_vm - -from doeff import Ask, Get, WithHandler, default_handlers, do, run +from doeff import Get, do from tests._run_helpers import run_with_defaults - - def test_probe_run_simple_program_returns_scalar() -> None: @do def program(): diff --git a/tests/core/test_sa008_spec_gaps.py b/tests/core/test_sa008_spec_gaps.py index 5b03cc971..982fd92fe 100644 --- a/tests/core/test_sa008_spec_gaps.py +++ b/tests/core/test_sa008_spec_gaps.py @@ -7,12 +7,6 @@ from pathlib import Path -import pytest - -from doeff import run -from doeff import EffectBase - - ROOT = Path(__file__).resolve().parents[2] RUST_SRC = ROOT / "packages" / "doeff-vm" / "src" RUST_CORE_SRC = ROOT / "packages" / "doeff-vm-core" / "src" @@ -32,35 +26,35 @@ def _read(path: Path) -> str: return path.read_text(encoding="utf-8") -def test_SA_008_G01_no_yielded_unknown_variant() -> None: +def test_SA_008_G01_no_yielded_unknown_variant() -> None: # noqa: N802 - public or spec test name is intentionally stable src_dir = ROOT / "packages" / "doeff-vm" / "src" assert not (src_dir / "yielded.rs").exists() -def test_SA_008_G02_classifier_no_unknown_fallback_branch() -> None: +def test_SA_008_G02_classifier_no_unknown_fallback_branch() -> None: # noqa: N802 - public or spec test name is intentionally stable src = _read(ROOT / "packages" / "doeff-vm" / "src" / "pyvm.rs") legacy_enum_prefix = "Yield" + "ed::" assert legacy_enum_prefix not in src -def test_SA_008_G03_no_dothunk_export_alias() -> None: +def test_SA_008_G03_no_dothunk_export_alias() -> None: # noqa: N802 - public or spec test name is intentionally stable src = _read(ROOT / "doeff" / "program.py") assert "DoThunk =" not in src -def test_SA_008_G04_map_flatmap_not_generator_wrapped() -> None: +def test_SA_008_G04_map_flatmap_not_generator_wrapped() -> None: # noqa: N802 - public or spec test name is intentionally stable src = _read(ROOT / "doeff" / "program.py") assert "GeneratorProgram(" not in src -def test_SA_008_G05_map_flatmap_runtime_not_unimplemented() -> None: +def test_SA_008_G05_map_flatmap_runtime_not_unimplemented() -> None: # noqa: N802 - public or spec test name is intentionally stable src = _read(ROOT / "packages" / "doeff-vm" / "src" / "vm.rs") assert "Map/FlatMap DoCtrl runtime evaluation is not implemented yet" not in src -def test_SA_008_G08_kpc_parse_not_shape_attribute_driven() -> None: +def test_SA_008_G08_kpc_parse_not_shape_attribute_driven() -> None: # noqa: N802 - public or spec test name is intentionally stable src = _read(ROOT / "doeff" / "program.py") assert "_annotation_is_program" not in src assert "_annotation_is_effect" not in src @@ -68,6 +62,6 @@ def test_SA_008_G08_kpc_parse_not_shape_attribute_driven() -> None: -def test_SA_008_G11_no_public_runtime_internal_export() -> None: +def test_SA_008_G11_no_public_runtime_internal_export() -> None: # noqa: N802 - public or spec test name is intentionally stable src = _read(ROOT / "packages" / "doeff-vm" / "doeff_vm" / "__init__.py") assert "PyVM" in src diff --git a/tests/core/test_sa009_async_handler_spec_gaps.py b/tests/core/test_sa009_async_handler_spec_gaps.py index 53faa6bcb..e3f1e5212 100644 --- a/tests/core/test_sa009_async_handler_spec_gaps.py +++ b/tests/core/test_sa009_async_handler_spec_gaps.py @@ -2,30 +2,18 @@ from __future__ import annotations -import asyncio from dataclasses import dataclass from pathlib import Path -import threading -import time from typing import Any -import pytest import doeff_vm from doeff import ( - Await, Effect, EffectBase, - Gather, - Spawn, - async_run, - default_async_handlers, - default_handlers, do, - run, ) - ROOT = Path(__file__).resolve().parents[2] RUST_SRC = ROOT / "packages" / "doeff-vm" / "src" CORE_EFFECTS_SRC = ROOT / "packages" / "doeff-core-effects" / "src" @@ -56,10 +44,10 @@ def _result_is_ok(run_result: object) -> bool: def _result_value(run_result: object) -> object: if hasattr(run_result, "value"): - return getattr(run_result, "value") + return run_result.value inner = getattr(run_result, "result", None) if inner is not None and hasattr(inner, "value"): - return getattr(inner, "value") + return inner.value raise AssertionError("RunResult does not expose value") @@ -76,7 +64,7 @@ def _assert_vm_handler_stack_matches_passed_handlers( """ expected_stack = [handler if callable(handler) else None for handler in reversed(passed_handlers)] assert len(vm_handler_stack) == len(expected_stack) - for expected, seen in zip(expected_stack, vm_handler_stack): + for expected, seen in zip(expected_stack, vm_handler_stack, strict=False): if expected is None: assert seen is None, f"Expected Rust sentinel identity placeholder None, got {seen!r}" else: @@ -106,7 +94,7 @@ class ProbeEffect(EffectBase): @staticmethod @do - def _passthrough_handler(effect: Effect, k): + def _passthrough_handler(_effect: Effect, _k): yield doeff_vm.Delegate() @staticmethod diff --git a/tests/core/test_scheduler_transfer.py b/tests/core/test_scheduler_transfer.py index dfcf2cedd..82cb89a9e 100644 --- a/tests/core/test_scheduler_transfer.py +++ b/tests/core/test_scheduler_transfer.py @@ -14,8 +14,8 @@ do, race, ) -from doeff_core_effects.scheduler import TaskCancelledError from tests._run_helpers import run_with_defaults + # REMOVED: from doeff.traceback import attach_doeff_traceback @@ -91,7 +91,7 @@ def program(): def test_many_concurrent_tasks_with_error_propagation() -> None: """Stress test: many tasks doing many yields, one crashes mid-execution. Verifies Transfer-based task switching doesn't corrupt error propagation - even under heavy concurrency (10 healthy × 20 yields + 1 crasher × 20 yields + even under heavy concurrency (10 healthy x 20 yields + 1 crasher x 20 yields = 220 total task switches). """ diff --git a/tests/core/test_spawn_gather_with_intercept.py b/tests/core/test_spawn_gather_with_intercept.py index 04097e2b6..ec7c280ff 100644 --- a/tests/core/test_spawn_gather_with_intercept.py +++ b/tests/core/test_spawn_gather_with_intercept.py @@ -16,33 +16,32 @@ """ import asyncio import time +from collections.abc import Callable from dataclasses import dataclass, field from datetime import datetime -from typing import Any, Callable, Dict, List +from typing import Any -import pytest +from doeff_core_effects.effects import EffectBase from doeff import ( + AcquireSemaphore, Ask, Await, CreateSemaphore, - AcquireSemaphore, Effect, - ReleaseSemaphore, EffectGenerator, Gather, Local, Pass, + ReleaseSemaphore, Resume, Spawn, Try, WithHandler, do, ) -from doeff_core_effects.effects import EffectBase from tests._run_helpers import run_with_defaults - # --- Custom effect (simulates HistoricalPriceEffect) --- @dataclass(frozen=True) @@ -57,7 +56,7 @@ class FetchEffect(EffectBase): @do def _query_fn( *, ticker: str, start_time: datetime, end_time: datetime -) -> EffectGenerator[List[Dict]]: +) -> EffectGenerator[list[dict]]: result = yield Await(asyncio.to_thread(lambda: [{"t": ticker, "v": 42}])) return result @@ -65,7 +64,7 @@ def _query_fn( @do def _sync_fn( *, ticker: str, start_time: datetime, end_time: datetime -) -> EffectGenerator[List[Dict]]: +) -> EffectGenerator[list[dict]]: return [] @@ -77,7 +76,7 @@ class CacheStrategy: @do def get_cached( self, ticker: str, start: datetime, end: datetime - ) -> EffectGenerator[List[Dict]]: + ) -> EffectGenerator[list[dict]]: raw = yield self.query_fn( ticker=ticker, start_time=start, end_time=end ) @@ -94,7 +93,7 @@ class PriceService: @do def fetch( self, ticker: str, start: datetime, end: datetime - ) -> EffectGenerator[List[Dict]]: + ) -> EffectGenerator[list[dict]]: cached = yield self.cache.get_cached(ticker, start, end) if cached: return cached @@ -131,7 +130,7 @@ def fetch_series( @do def compute_movement(ticker: str) -> EffectGenerator[Any]: - now = datetime.now() + now = datetime.now() # noqa: DTZ005 - existing local wall-clock behavior is intentionally unchanged df_result = yield Try(fetch_series(ticker, now, now)) if df_result.is_err(): raise RuntimeError(f"compute failed: {df_result.error}") @@ -190,7 +189,7 @@ def _build_program(n: int = 5, concurrency: int = 3, with_intercept: bool = Fals make_handler(), ) if with_intercept: - wrapped = WithIntercept(passthrough_interceptor, wrapped) + wrapped = WithIntercept(passthrough_interceptor, wrapped) # noqa: F821 - legacy removed API reference is intentionally preserved return Local(env, wrapped) diff --git a/tests/core/test_spawn_try_gather.py b/tests/core/test_spawn_try_gather.py index 261021888..875b9b123 100644 --- a/tests/core/test_spawn_try_gather.py +++ b/tests/core/test_spawn_try_gather.py @@ -12,21 +12,22 @@ from dataclasses import dataclass from typing import Any +from doeff_core_effects.effects import EffectBase + from doeff import ( - CreateSemaphore, AcquireSemaphore, - ReleaseSemaphore, + CreateSemaphore, Effect, EffectGenerator, Gather, Pass, + ReleaseSemaphore, Resume, Spawn, Try, WithHandler, do, ) -from doeff_core_effects.effects import EffectBase from tests._run_helpers import run_with_defaults ProgramLike = Any # removed API shim diff --git a/tests/core/test_spec_gaps.py b/tests/core/test_spec_gaps.py index 129a904a0..4037c3363 100644 --- a/tests/core/test_spec_gaps.py +++ b/tests/core/test_spec_gaps.py @@ -12,10 +12,6 @@ import re from pathlib import Path -import pytest - -from doeff import Ask, Get, Put, Tell, default_handlers, run -from tests._run_helpers import run_with_defaults # REMOVED: from doeff.program import GeneratorProgram ROOT = Path(__file__).resolve().parents[2] @@ -37,7 +33,7 @@ def _read_vm_or_core_effects(filename: str) -> str: def _prog(gen_factory): """Wrap a generator factory into a GeneratorProgram (has to_generator).""" - return GeneratorProgram(gen_factory) + return GeneratorProgram(gen_factory) # noqa: F821 - legacy removed API reference is intentionally preserved # --------------------------------------------------------------------------- @@ -52,31 +48,31 @@ class TestG8ImportPaths: def test_import_with_handler(self) -> None: """from doeff import WithHandler must succeed.""" - from doeff import WithHandler # noqa: F401 + from doeff import WithHandler assert WithHandler is not None def test_import_resume(self) -> None: """from doeff import Resume must succeed.""" - from doeff import Resume # noqa: F401 + from doeff import Resume assert Resume is not None def test_import_delegate(self) -> None: """from doeff import Delegate must succeed.""" - from doeff import Delegate # noqa: F401 + from doeff import Delegate assert Delegate is not None def test_import_transfer(self) -> None: """from doeff import Transfer must succeed.""" - from doeff import Transfer # noqa: F401 + from doeff import Transfer assert Transfer is not None def test_import_k(self) -> None: """from doeff import K must succeed.""" - from doeff import K # noqa: F401 + from doeff import K assert K is not None @@ -93,19 +89,19 @@ class TestG9HandlersModule: def test_import_state(self) -> None: """from doeff.handlers import state must succeed.""" - from doeff_core_effects.handlers import state # noqa: F401 + from doeff_core_effects.handlers import state assert state is not None def test_import_reader(self) -> None: """from doeff.handlers import reader must succeed.""" - from doeff_core_effects.handlers import reader # noqa: F401 + from doeff_core_effects.handlers import reader assert reader is not None def test_import_writer(self) -> None: """from doeff.handlers import writer must succeed.""" - from doeff_core_effects.handlers import writer # noqa: F401 + from doeff_core_effects.handlers import writer assert writer is not None diff --git a/tests/core/test_spec_trace_001_examples.py b/tests/core/test_spec_trace_001_examples.py index 8fd40117e..dadc4ea69 100644 --- a/tests/core/test_spec_trace_001_examples.py +++ b/tests/core/test_spec_trace_001_examples.py @@ -2,13 +2,8 @@ import inspect -import pytest - -from doeff import Ask, Effect, Pass, Program, Resume, WithHandler, default_handlers, do, run -from doeff import Put -from doeff_core_effects.scheduler import Gather -from doeff_core_effects.scheduler import Spawn from tests._run_helpers import run_with_defaults + # REMOVED: from doeff.traceback import attach_doeff_traceback _DEFAULT_HANDLER_NAMES = ( @@ -105,6 +100,6 @@ def _render_failure( ) -> str: result = run_with_defaults(program, env=env, store=store) assert result.is_err() - doeff_tb = attach_doeff_traceback(result.error, traceback_data=result.traceback_data) + doeff_tb = attach_doeff_traceback(result.error, traceback_data=result.traceback_data) # noqa: F821 - legacy removed API reference is intentionally preserved assert doeff_tb is not None return doeff_tb.format_default() diff --git a/tests/core/test_trace_coercion_strict.py b/tests/core/test_trace_coercion_strict.py index 5a8983dd0..2fa45c75d 100644 --- a/tests/core/test_trace_coercion_strict.py +++ b/tests/core/test_trace_coercion_strict.py @@ -1,6 +1,5 @@ from __future__ import annotations -import pytest # REMOVED: from doeff.trace import ( # ContextEntry, diff --git a/tests/core/test_unified_traceback_system.py b/tests/core/test_unified_traceback_system.py index 62add03f3..42b9872fe 100644 --- a/tests/core/test_unified_traceback_system.py +++ b/tests/core/test_unified_traceback_system.py @@ -1,17 +1,13 @@ from __future__ import annotations -import warnings from dataclasses import dataclass -import doeff_vm -import pytest +from doeff_vm import Pass, Resume, WithHandler -from doeff import Delegate, Effect, Program, default_handlers, do, run -from doeff import EffectBase # REMOVED: from doeff import ProgramCallStack -from doeff import Put -from doeff_vm import Pass, Resume, WithHandler +from doeff import Effect, EffectBase, Program, do from tests._run_helpers import run_with_defaults + # REMOVED: from doeff.trace import TraceDispatch # REMOVED: from doeff.traceback import attach_doeff_traceback, get_attached_doeff_traceback diff --git a/tests/core/test_vm_architecture_ocaml5.py b/tests/core/test_vm_architecture_ocaml5.py index 2290cd1d3..790dac3f2 100644 --- a/tests/core/test_vm_architecture_ocaml5.py +++ b/tests/core/test_vm_architecture_ocaml5.py @@ -13,16 +13,15 @@ from doeff import ( Effect, EffectBase, + Gather, Pass, Resume, Spawn, - Gather, WithHandler, do, ) from tests._run_helpers import run_with_defaults - # --------------------------------------------------------------------------- # Continuation move semantics: a fiber is in ONE place, never both # --------------------------------------------------------------------------- @@ -112,7 +111,6 @@ class GetShared(SharedState): class PutShared(SharedState): - pass def __init__(self, value): self.value = value @@ -217,7 +215,6 @@ def test_dispatch_does_not_corrupt_shared_handler_chain(): class TaskEffect(EffectBase): pass - results = [] @do def _handler(effect: Effect, k): diff --git a/tests/core/test_vm_architecture_ocaml5_source.py b/tests/core/test_vm_architecture_ocaml5_source.py index 43856bbd2..a9a9a5d43 100644 --- a/tests/core/test_vm_architecture_ocaml5_source.py +++ b/tests/core/test_vm_architecture_ocaml5_source.py @@ -1,8 +1,6 @@ from __future__ import annotations from pathlib import Path -import re - ROOT = Path(__file__).resolve().parents[2] SEGMENT_RS = ROOT / "packages" / "doeff-vm-core" / "src" / "segment.rs" diff --git a/tests/core/test_vm_core_handle_indirection.py b/tests/core/test_vm_core_handle_indirection.py index 9f6cb31c7..2d6b7fa6e 100644 --- a/tests/core/test_vm_core_handle_indirection.py +++ b/tests/core/test_vm_core_handle_indirection.py @@ -1,8 +1,7 @@ from __future__ import annotations -from pathlib import Path import re - +from pathlib import Path ROOT = Path(__file__).resolve().parents[2] VM_CORE_CARGO = ROOT / "packages" / "doeff-vm-core" / "Cargo.toml" @@ -22,7 +21,7 @@ def _runtime_source(path: Path) -> str: def test_vm_core_pyo3_dependency_is_optional() -> None: cargo = VM_CORE_CARGO.read_text(encoding="utf-8") - assert re.search(r'^pyo3\s*=\s*\{[^\n}]*optional\s*=\s*true', cargo, re.MULTILINE), ( + assert re.search(r"^pyo3\s*=\s*\{[^\n}]*optional\s*=\s*true", cargo, re.MULTILINE), ( "doeff-vm-core must make pyo3 optional so --no-default-features builds stay GIL-free" ) diff --git a/tests/core/test_vm_fiber_ownership_g1.py b/tests/core/test_vm_fiber_ownership_g1.py index efc9a202f..4ea837b44 100644 --- a/tests/core/test_vm_fiber_ownership_g1.py +++ b/tests/core/test_vm_fiber_ownership_g1.py @@ -1,8 +1,7 @@ from __future__ import annotations -from pathlib import Path import re - +from pathlib import Path ROOT = Path(__file__).resolve().parents[2] VM_CORE = ROOT / "packages" / "doeff-vm-core" / "src" diff --git a/tests/core/test_vm_move_semantics_regressions.py b/tests/core/test_vm_move_semantics_regressions.py index 607a17987..4ff350f4f 100644 --- a/tests/core/test_vm_move_semantics_regressions.py +++ b/tests/core/test_vm_move_semantics_regressions.py @@ -1,6 +1,6 @@ from __future__ import annotations -from doeff import Ask, MissingEnvKeyError, default_handlers, do, run +from doeff import Ask, MissingEnvKeyError, do from tests._run_helpers import run_with_defaults diff --git a/tests/core/test_vm_mutable_state_audit_g0.py b/tests/core/test_vm_mutable_state_audit_g0.py index 0b1a01f29..3dcba4a3a 100644 --- a/tests/core/test_vm_mutable_state_audit_g0.py +++ b/tests/core/test_vm_mutable_state_audit_g0.py @@ -1,8 +1,7 @@ from __future__ import annotations -from pathlib import Path import re - +from pathlib import Path ROOT = Path(__file__).resolve().parents[2] VM_RS = ROOT / "packages" / "doeff-vm-core" / "src" / "vm.rs" diff --git a/tests/core/test_vm_ocaml5_violations.py b/tests/core/test_vm_ocaml5_violations.py index 93b064365..bdd453335 100644 --- a/tests/core/test_vm_ocaml5_violations.py +++ b/tests/core/test_vm_ocaml5_violations.py @@ -13,8 +13,8 @@ """ from __future__ import annotations -from pathlib import Path import re +from pathlib import Path ROOT = Path(__file__).resolve().parents[2] SEGMENT_RS = ROOT / "packages" / "doeff-vm-core" / "src" / "segment.rs" diff --git a/tests/effects/test_await_asyncio_semaphore_deadlock.py b/tests/effects/test_await_asyncio_semaphore_deadlock.py index 67a1ce771..758178c86 100644 --- a/tests/effects/test_await_asyncio_semaphore_deadlock.py +++ b/tests/effects/test_await_asyncio_semaphore_deadlock.py @@ -12,17 +12,15 @@ import signal from typing import Any -import pytest - from doeff import ( AcquireSemaphore, + Await, CreateSemaphore, Gather, ReleaseSemaphore, Spawn, do, ) -from doeff import Await from tests._run_helpers import run_with_defaults DEADLOCK_TIMEOUT_SECONDS = 5 diff --git a/tests/effects/test_compat_cleanup_spec.py b/tests/effects/test_compat_cleanup_spec.py index d905ce585..1eb613275 100644 --- a/tests/effects/test_compat_cleanup_spec.py +++ b/tests/effects/test_compat_cleanup_spec.py @@ -1,11 +1,5 @@ from __future__ import annotations -import pytest - -import pytest - -from doeff import Program -from doeff_core_effects.scheduler import Spawn # REMOVED: from doeff_core_effects.scheduler import spawn diff --git a/tests/effects/test_control_effects.py b/tests/effects/test_control_effects.py index bca6a90a1..96a1cec4d 100644 --- a/tests/effects/test_control_effects.py +++ b/tests/effects/test_control_effects.py @@ -13,20 +13,20 @@ Reference: gh#177 """ -import pytest - -from doeff import Program, Spawn, WithIntercept, do -from tests._run_helpers import run_with_defaults from doeff import ( Ask, Gather, Get, Local, + Program, Pure, Put, + Spawn, Try, + WithIntercept, + do, ) -from doeff_core_effects.effects import Ask +from tests._run_helpers import run_with_defaults def _with_legacy_intercept_chain(program: Program, *transforms): diff --git a/tests/effects/test_effect_combinations.py b/tests/effects/test_effect_combinations.py index 2a8a70290..f40f82036 100644 --- a/tests/effects/test_effect_combinations.py +++ b/tests/effects/test_effect_combinations.py @@ -12,8 +12,6 @@ import pytest -from doeff import Program, do -from tests._run_helpers import run_with_defaults from doeff import ( Ask, Await, @@ -25,7 +23,9 @@ Spawn, Tell, Try, + do, ) +from tests._run_helpers import run_with_defaults # ============================================================================ # Law 1: Local Restoration Law Tests diff --git a/tests/effects/test_env_var_ask.py b/tests/effects/test_env_var_ask.py index 90171adb2..303c0b931 100644 --- a/tests/effects/test_env_var_ask.py +++ b/tests/effects/test_env_var_ask.py @@ -15,11 +15,10 @@ from __future__ import annotations import pytest - -from doeff import Ask, Pass, Resume, WithHandler, do, run from doeff_core_effects.handlers import env_var_ask, lazy_ask -from doeff_core_effects.scheduler import Gather, Spawn, Wait, scheduled +from doeff_core_effects.scheduler import Gather, Spawn, scheduled +from doeff import Ask, Pass, Resume, WithHandler, do, run # --- Plain string values --------------------------------------------------- diff --git a/tests/effects/test_external_promise.py b/tests/effects/test_external_promise.py index 3bcaa6160..1cd6bbd9c 100644 --- a/tests/effects/test_external_promise.py +++ b/tests/effects/test_external_promise.py @@ -8,10 +8,7 @@ import time from typing import Any -import pytest - -from doeff import Gather, Spawn, do -from doeff import CreateExternalPromise, Wait +from doeff import CreateExternalPromise, Gather, Spawn, Wait, do from tests._run_helpers import run_with_defaults diff --git a/tests/effects/test_lazy_ask.py b/tests/effects/test_lazy_ask.py index 690c56fd7..dc0c58d98 100644 --- a/tests/effects/test_lazy_ask.py +++ b/tests/effects/test_lazy_ask.py @@ -1,11 +1,11 @@ """Tests for lazy_ask handler — SPEC-EFF-001 lazy program evaluation.""" -from doeff import do, run -from doeff.program import WithHandler - from doeff_core_effects.effects import Ask, Local, Try from doeff_core_effects.handlers import lazy_ask, try_handler -from doeff_core_effects.scheduler import scheduled, Spawn, Gather, Wait +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): @@ -356,7 +356,8 @@ def program(): return (yield Try(Ask("svc"))) result = run_with_lazy(program(), env={"svc": failing_service()}) - assert hasattr(result, "error") and isinstance(result.error, ValueError) + assert hasattr(result, "error") + assert isinstance(result.error, ValueError) def test_missing_key_error_strict(self): """strict=True preserves legacy KeyError-on-miss behaviour.""" @@ -367,4 +368,5 @@ def program(): body = WithHandler(try_handler, program()) body = WithHandler(lazy_ask(env={}, strict=True), body) result = run(scheduled(body)) - assert hasattr(result, "error") and isinstance(result.error, KeyError) + 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 184195d43..c72b5e568 100644 --- a/tests/effects/test_lazy_ask_env_var_fallback.py +++ b/tests/effects/test_lazy_ask_env_var_fallback.py @@ -27,7 +27,8 @@ import os -import pytest +from doeff_core_effects.handlers import lazy_ask +from doeff_core_effects.scheduler import scheduled from doeff import ( Ask, @@ -37,9 +38,6 @@ do, run, ) -from doeff_core_effects.handlers import lazy_ask -from doeff_core_effects.scheduler import scheduled - # --- env_var_fallback_handler: resolves Ask from os.environ, passes otherwise --- diff --git a/tests/effects/test_lazy_ask_inner_handler_propagation.py b/tests/effects/test_lazy_ask_inner_handler_propagation.py index 0aae9b41f..4cb9a696b 100644 --- a/tests/effects/test_lazy_ask_inner_handler_propagation.py +++ b/tests/effects/test_lazy_ask_inner_handler_propagation.py @@ -33,7 +33,8 @@ from dataclasses import dataclass -import pytest +from doeff_core_effects.handlers import lazy_ask, state, try_handler, writer +from doeff_core_effects.scheduler import scheduled from doeff import ( Ask, @@ -44,9 +45,6 @@ do, run, ) -from doeff_core_effects.handlers import lazy_ask, state, try_handler, writer -from doeff_core_effects.scheduler import scheduled - # --- Custom effects simulating GCP Secret Manager --- diff --git a/tests/effects/test_lazy_ask_semaphore.py b/tests/effects/test_lazy_ask_semaphore.py index 760eaabfd..d85f6c7ee 100644 --- a/tests/effects/test_lazy_ask_semaphore.py +++ b/tests/effects/test_lazy_ask_semaphore.py @@ -2,8 +2,6 @@ from pathlib import Path -import pytest - from doeff import ( Ask, Gather, @@ -13,8 +11,6 @@ Wait, do, ) -from doeff_core_effects.handlers import lazy_ask, reader, state -from doeff_core_effects.scheduler import scheduled as scheduler from tests._run_helpers import run_with_defaults ROOT = Path(__file__).resolve().parents[2] diff --git a/tests/effects/test_pure_effect.py b/tests/effects/test_pure_effect.py index 34aa09274..f25abbced 100644 --- a/tests/effects/test_pure_effect.py +++ b/tests/effects/test_pure_effect.py @@ -1,9 +1,9 @@ """Tests for Pure - the immediate value effect (Pure case of Free monad).""" import pytest +from doeff_vm import Pure from doeff import Program, do -from doeff_vm import Pure from tests._run_helpers import run_with_defaults diff --git a/tests/effects/test_semaphore.py b/tests/effects/test_semaphore.py index 27e1eec08..7b4827cc7 100644 --- a/tests/effects/test_semaphore.py +++ b/tests/effects/test_semaphore.py @@ -11,13 +11,12 @@ ReleaseSemaphore, Semaphore, Spawn, - Try, Wait, do, ) -from doeff_core_effects.scheduler import TaskCancelledError from tests._run_helpers import run_with_defaults + def test_semaphore_public_handle_has_no_runtime_cleanup_surface() -> None: import doeff_vm diff --git a/tests/effects/test_spawn_priority.py b/tests/effects/test_spawn_priority.py index 11117b525..0dfb34fec 100644 --- a/tests/effects/test_spawn_priority.py +++ b/tests/effects/test_spawn_priority.py @@ -3,8 +3,6 @@ import asyncio from typing import Any -import pytest - import doeff from doeff import ( PRIORITY_HIGH, diff --git a/tests/effects/test_sync_await_spawn_gather_deadlock.py b/tests/effects/test_sync_await_spawn_gather_deadlock.py index 585f93c18..a93dbd35c 100644 --- a/tests/effects/test_sync_await_spawn_gather_deadlock.py +++ b/tests/effects/test_sync_await_spawn_gather_deadlock.py @@ -18,17 +18,15 @@ import signal from typing import Any -import pytest - from doeff import ( AcquireSemaphore, + Await, CreateSemaphore, Gather, ReleaseSemaphore, Spawn, do, ) -from doeff import Await from tests._run_helpers import run_with_defaults TIMEOUT_SECONDS = 10 diff --git a/tests/effects/test_traverse.py b/tests/effects/test_traverse.py index 0be7defc1..4bb424bb1 100644 --- a/tests/effects/test_traverse.py +++ b/tests/effects/test_traverse.py @@ -1,15 +1,14 @@ """Tests for doeff-traverse: Fail, Traverse, Reduce, Zip, Inspect, Skip, SortBy, Take.""" -from doeff import do, run -from doeff.program import WithHandler - from doeff_core_effects.handlers import try_handler from doeff_core_effects.scheduler import scheduled - -from doeff_traverse.effects import Fail, Traverse, Reduce, Zip, Inspect, Skip, SortBy, Take -from doeff_traverse.handlers import sequential, normalize_to_none, fail_handler -from doeff_traverse.helpers import try_call from doeff_traverse.collection import Collection +from doeff_traverse.effects import Fail, Inspect, Reduce, Skip, SortBy, Take, Traverse, Zip +from doeff_traverse.handlers import fail_handler, normalize_to_none, sequential +from doeff_traverse.helpers import try_call + +from doeff import do, run +from doeff.program import WithHandler def run_with(program, handlers=None): @@ -88,7 +87,7 @@ def program(): return (yield try_call(int, "not_a_number")) import pytest - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="invalid literal"): run_with(program()) def test_try_call_failure_normalized(self): @@ -263,7 +262,7 @@ def test_zip_simple(self): """Zip combines two collections by index.""" @do def program(): - a = yield Traverse(lambda x: _pure(x), [1, 2, 3]) + a = yield Traverse(_pure, [1, 2, 3]) b = yield Traverse(lambda x: _pure(x * 10), [1, 2, 3]) return (yield Zip(a, b)) @@ -488,7 +487,7 @@ def test_sort_ascending(self): """SortBy sorts valid items by key.""" @do def program(): - col = yield Traverse(lambda x: _pure(x), [3, 1, 4, 1, 5]) + col = yield Traverse(_pure, [3, 1, 4, 1, 5]) return (yield SortBy(lambda x: x, col)) col = run_with(program()) @@ -498,7 +497,7 @@ def test_sort_descending(self): """SortBy with reverse=True sorts descending.""" @do def program(): - col = yield Traverse(lambda x: _pure(x), [3, 1, 4, 1, 5]) + col = yield Traverse(_pure, [3, 1, 4, 1, 5]) return (yield SortBy(lambda x: x, col, reverse=True)) col = run_with(program()) @@ -527,7 +526,7 @@ def test_sort_by_key(self): """SortBy with a key function.""" @do def program(): - col = yield Traverse(lambda x: _pure(x), ["banana", "apple", "cherry"]) + col = yield Traverse(_pure, ["banana", "apple", "cherry"]) return (yield SortBy(len, col)) col = run_with(program()) @@ -543,7 +542,7 @@ def test_take_n(self): """Take returns first n valid items.""" @do def program(): - col = yield Traverse(lambda x: _pure(x), [10, 20, 30, 40, 50]) + col = yield Traverse(_pure, [10, 20, 30, 40, 50]) return (yield Take(3, col)) col = run_with(program()) @@ -554,7 +553,7 @@ def test_take_more_than_available(self): """Take with n > len returns all items.""" @do def program(): - col = yield Traverse(lambda x: _pure(x), [1, 2]) + col = yield Traverse(_pure, [1, 2]) return (yield Take(10, col)) col = run_with(program()) @@ -603,7 +602,7 @@ def test_sort_then_take(self): """SortBy + Take composes: top-N pattern.""" @do def program(): - col = yield Traverse(lambda x: _pure(x), [5, 3, 8, 1, 9, 2]) + col = yield Traverse(_pure, [5, 3, 8, 1, 9, 2]) sorted_col = yield SortBy(lambda x: x, col, reverse=True) return (yield Take(3, sorted_col)) diff --git a/tests/fixtures_discovery/myapp/__init__.py b/tests/fixtures_discovery/myapp/__init__.py index cc2a6bab3..a8c6919ed 100644 --- a/tests/fixtures_discovery/myapp/__init__.py +++ b/tests/fixtures_discovery/myapp/__init__.py @@ -1,6 +1,8 @@ from __future__ import annotations -from doeff import Program, default_handlers, run +from doeff import Program +from doeff import default_handlers as default_handlers +from doeff import run as run from tests._run_helpers import run_with_defaults diff --git a/tests/fixtures_discovery/myapp/features/__init__.py b/tests/fixtures_discovery/myapp/features/__init__.py index 558fcc14b..370ea2e91 100644 --- a/tests/fixtures_discovery/myapp/features/__init__.py +++ b/tests/fixtures_discovery/myapp/features/__init__.py @@ -2,6 +2,5 @@ from doeff import Program - # doeff: default features_env: Program[dict] = Program.pure({"feature_flag": True}) diff --git a/tests/fixtures_discovery/myapp/features/auth/__init__.py b/tests/fixtures_discovery/myapp/features/auth/__init__.py index 93cfd3465..a125a10f8 100644 --- a/tests/fixtures_discovery/myapp/features/auth/__init__.py +++ b/tests/fixtures_discovery/myapp/features/auth/__init__.py @@ -1,6 +1,8 @@ from __future__ import annotations -from doeff import Program, default_handlers, run +from doeff import Program +from doeff import default_handlers as default_handlers +from doeff import run as run from tests._run_helpers import run_with_defaults diff --git a/tests/misc/test_reader_hashable_keys.py b/tests/misc/test_reader_hashable_keys.py index badc4c29b..9bc450452 100644 --- a/tests/misc/test_reader_hashable_keys.py +++ b/tests/misc/test_reader_hashable_keys.py @@ -1,7 +1,5 @@ from __future__ import annotations -import pytest - from doeff import Ask, do from tests._run_helpers import run_with_defaults diff --git a/tests/public_api/test_api_cleanup.py b/tests/public_api/test_api_cleanup.py index aa456a895..bb3b2e945 100644 --- a/tests/public_api/test_api_cleanup.py +++ b/tests/public_api/test_api_cleanup.py @@ -5,6 +5,7 @@ import importlib import pytest + from tests._run_helpers import run_with_defaults pytestmark = pytest.mark.skip( @@ -67,7 +68,7 @@ def test_safe_not_in_all() -> None: def test_try_catches_error() -> None: """Try wraps sub-program errors into Result.""" - from doeff import Try, default_handlers, do, run + from doeff import Try, do @do def failing(): diff --git a/tests/public_api/test_do_bang_defp_defpp.py b/tests/public_api/test_do_bang_defp_defpp.py index 6b3ad6c20..fc118f285 100644 --- a/tests/public_api/test_do_bang_defp_defpp.py +++ b/tests/public_api/test_do_bang_defp_defpp.py @@ -11,10 +11,9 @@ from typing import Any import pytest - -from doeff import Ask, WithHandler, do, run from doeff_core_effects import reader +from doeff import Ask, WithHandler, do, run # --------------------------------------------------------------------------- # do! — returns a Program (generator), usable anywhere @@ -81,7 +80,7 @@ def test_defp_guard_detects_program_return(self) -> None: def make_bad_program(): """A program whose return value is itself a generator.""" x = yield Ask("key") - return (y for y in [x]) # noqa: C400 — intentional generator + return (y for y in [x]) result = run(WithHandler(reader(env={"key": "v"}), make_bad_program())) # The return value IS a generator — defp's guard would catch this @@ -177,8 +176,8 @@ class TestImportInsideDefpDefk: @pytest.fixture(autouse=True) def _import_repro(self) -> None: - import sys import os + import sys import doeff_hy # noqa: F401 import hy # noqa: F401 diff --git a/tests/public_api/test_doeff13_hang_regression.py b/tests/public_api/test_doeff13_hang_regression.py index e21d245f4..271164bca 100644 --- a/tests/public_api/test_doeff13_hang_regression.py +++ b/tests/public_api/test_doeff13_hang_regression.py @@ -20,11 +20,10 @@ from doeff import ( Effect, EffectBase, - Pass, Resume, WithHandler, do, - run,) +) from tests._run_helpers import run_with_defaults # --------------------------------------------------------------------------- diff --git a/tests/public_api/test_kpc_macro_expansion.py b/tests/public_api/test_kpc_macro_expansion.py index ba508066e..8ac001bc3 100644 --- a/tests/public_api/test_kpc_macro_expansion.py +++ b/tests/public_api/test_kpc_macro_expansion.py @@ -3,8 +3,9 @@ from collections.abc import Generator from typing import Any -from doeff import Ask, Effect, Program, do -from doeff_vm import Expand, Perform, Pure +from doeff_vm import Expand + +from doeff import Effect, Program, do @do diff --git a/tests/public_api/test_kpc_macro_runtime_contract.py b/tests/public_api/test_kpc_macro_runtime_contract.py index ec0a94e64..228639423 100644 --- a/tests/public_api/test_kpc_macro_runtime_contract.py +++ b/tests/public_api/test_kpc_macro_runtime_contract.py @@ -8,8 +8,7 @@ import doeff_vm -from doeff import ProgramBase, default_handlers, do, presets, run -from tests._run_helpers import run_with_defaults +from doeff import ProgramBase, do class TestNoLegacyKpcExports: diff --git a/tests/public_api/test_mcp_tool_def.py b/tests/public_api/test_mcp_tool_def.py index 7a31db9a9..dd6b7c82e 100644 --- a/tests/public_api/test_mcp_tool_def.py +++ b/tests/public_api/test_mcp_tool_def.py @@ -1,11 +1,11 @@ """Tests for McpToolDef and McpParamSchema dataclasses.""" -from doeff.mcp import McpParamSchema, McpToolDef, _MISSING +from doeff.mcp import _MISSING, McpParamSchema, McpToolDef def _dummy_handler(x, y): """Placeholder handler for testing.""" - return None + return class TestMcpParamSchema: @@ -58,7 +58,7 @@ def test_frozen(self): p = McpParamSchema(name="x", type="string", description="test") try: p.name = "y" - assert False, "should be frozen" + raise AssertionError("should be frozen") except AttributeError: pass @@ -123,7 +123,7 @@ def test_frozen(self): tool = self._make_tool() try: tool.name = "x" - assert False, "should be frozen" + raise AssertionError("should be frozen") except AttributeError: pass diff --git a/tests/public_api/test_mcp_tool_macro.py b/tests/public_api/test_mcp_tool_macro.py index 9af67f07c..c2ff31e91 100644 --- a/tests/public_api/test_mcp_tool_macro.py +++ b/tests/public_api/test_mcp_tool_macro.py @@ -3,14 +3,14 @@ import sys import types +import doeff_hy # noqa — registers extensions import hy import hy.macros import pytest +from doeff_vm import Expand -import doeff_hy # noqa — registers extensions from doeff import do, run -from doeff.mcp import McpToolDef, McpParamSchema -from doeff_vm import Expand +from doeff.mcp import McpToolDef def _eval_mcp(code: str, **extra_globals): diff --git a/tests/public_api/test_types_001_composition.py b/tests/public_api/test_types_001_composition.py index e9732c653..77b5c3456 100644 --- a/tests/public_api/test_types_001_composition.py +++ b/tests/public_api/test_types_001_composition.py @@ -2,10 +2,7 @@ from __future__ import annotations -import pytest -import doeff_vm - -from doeff import Ask, Get, Program, default_handlers, do, run +from doeff import Ask, Get, do from tests._run_helpers import run_with_defaults diff --git a/tests/public_api/test_types_001_handler_protocol.py b/tests/public_api/test_types_001_handler_protocol.py index f7836db9a..a88dd4f71 100644 --- a/tests/public_api/test_types_001_handler_protocol.py +++ b/tests/public_api/test_types_001_handler_protocol.py @@ -7,20 +7,18 @@ from typing import Any -import pytest - from doeff import ( Ask, Effect, EffectBase, Get, Pass, - Put, Pure, + Put, Resume, WithHandler, do, - run,) +) from tests._run_helpers import run_with_defaults diff --git a/tests/public_api/test_types_001_validation.py b/tests/public_api/test_types_001_validation.py index 08680e060..97db89d6e 100644 --- a/tests/public_api/test_types_001_validation.py +++ b/tests/public_api/test_types_001_validation.py @@ -10,7 +10,6 @@ from typing import Any, cast -import doeff import pytest from doeff import ( diff --git a/tests/test_architecture_violations.py b/tests/test_architecture_violations.py index 412f1195c..c0f031601 100644 --- a/tests/test_architecture_violations.py +++ b/tests/test_architecture_violations.py @@ -8,8 +8,8 @@ No accumulated state, no identity tracking, no stored fiber ID lists. """ -import subprocess import os +import subprocess REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) VM_CORE_SRC = os.path.join(REPO_ROOT, "packages", "doeff-vm-core", "src") @@ -27,6 +27,7 @@ def _grep_rust(pattern: str, dirs: list[str] | None = None) -> list[str]: result = subprocess.run( ["rg", "--no-heading", "-n", "--type", "rs", pattern] + existing, capture_output=True, text=True, + check=False, ) return [line for line in result.stdout.strip().split("\n") if line] @@ -42,7 +43,7 @@ def _filter_non_test_non_comment(lines: list[str]) -> list[str]: parts = line.split(":", 2) if len(parts) >= 3: code = parts[2].strip() - if code.startswith("//") or code.startswith("///"): + if code.startswith(("//", "///")): continue filtered.append(line) return filtered @@ -54,14 +55,14 @@ class TestNoContId: def test_no_cont_id_struct(self): matches = _grep_rust(r"struct ContId") matches = _filter_non_test_non_comment(matches) - assert matches == [], f"ContId struct found:\n" + "\n".join(matches) + assert matches == [], "ContId struct found:\n" + "\n".join(matches) def test_no_cont_id_usage(self): matches = _grep_rust(r"ContId(?!.*removed)") matches = _filter_non_test_non_comment(matches) # Filter out the "// ContId removed" comments matches = [m for m in matches if "removed" not in m and "ContId" in m.split(":", 2)[-1]] - assert matches == [], f"ContId usage found:\n" + "\n".join(matches) + assert matches == [], "ContId usage found:\n" + "\n".join(matches) class TestNoCopyMachine: @@ -70,17 +71,17 @@ class TestNoCopyMachine: def test_no_clone_handle(self): matches = _grep_rust(r"clone_handle\(\)") matches = _filter_non_test_non_comment(matches) - assert matches == [], f"clone_handle() found:\n" + "\n".join(matches) + assert matches == [], "clone_handle() found:\n" + "\n".join(matches) def test_no_capture_from_fiber_ids(self): matches = _grep_rust(r"capture_from_fiber_ids") matches = _filter_non_test_non_comment(matches) - assert matches == [], f"capture_from_fiber_ids found:\n" + "\n".join(matches) + assert matches == [], "capture_from_fiber_ids found:\n" + "\n".join(matches) def test_no_continuation_from_topology(self): matches = _grep_rust(r"continuation_from_topology") matches = _filter_non_test_non_comment(matches) - assert matches == [], f"continuation_from_topology found:\n" + "\n".join(matches) + assert matches == [], "continuation_from_topology found:\n" + "\n".join(matches) class TestNoSharedOwnership: @@ -90,14 +91,14 @@ def test_no_arc_atomicbool_in_continuation(self): matches = _grep_rust(r"Arc.*AtomicBool", dirs=[VM_CORE_SRC]) # Only check continuation.rs matches = [m for m in matches if "continuation.rs" in m] - assert matches == [], f"Arc in continuation:\n" + "\n".join(matches) + assert matches == [], "Arc in continuation:\n" + "\n".join(matches) def test_continuation_not_clone(self): """Continuation must not derive or implement Clone.""" matches = _grep_rust(r"impl Clone for Continuation", dirs=[VM_CORE_SRC]) matches += _grep_rust(r"derive.*Clone.*Continuation|Continuation.*derive.*Clone", dirs=[VM_CORE_SRC]) matches = _filter_non_test_non_comment(matches) - assert matches == [], f"Continuation implements Clone:\n" + "\n".join(matches) + assert matches == [], "Continuation implements Clone:\n" + "\n".join(matches) class TestNoStoredFiberIdLists: @@ -123,7 +124,7 @@ def test_no_vec_fiber_id_on_structs(self): continue violations.append(m) assert violations == [], ( - f"Vec stored on struct (should use FiberId head + parent walk):\n" + "Vec stored on struct (should use FiberId head + parent walk):\n" + "\n".join(violations) ) @@ -134,7 +135,7 @@ class TestNoProgramDispatch: def test_no_program_dispatch_struct(self): matches = _grep_rust(r"struct ProgramDispatch") matches = _filter_non_test_non_comment(matches) - assert matches == [], f"ProgramDispatch struct found:\n" + "\n".join(matches) + assert matches == [], "ProgramDispatch struct found:\n" + "\n".join(matches) class TestNoAccumulatedTraceState: @@ -143,12 +144,12 @@ class TestNoAccumulatedTraceState: def test_no_trace_state(self): matches = _grep_rust(r"TraceState", dirs=[VM_CORE_SRC]) matches = _filter_non_test_non_comment(matches) - assert matches == [], f"TraceState found:\n" + "\n".join(matches) + assert matches == [], "TraceState found:\n" + "\n".join(matches) def test_no_debug_state_on_vm(self): matches = _grep_rust(r"debug.*DebugState|DebugState.*debug", dirs=[VM_CORE_SRC]) matches = _filter_non_test_non_comment(matches) - assert matches == [], f"DebugState on VM found:\n" + "\n".join(matches) + assert matches == [], "DebugState on VM found:\n" + "\n".join(matches) class TestFiberThreeFields: @@ -160,11 +161,11 @@ def test_no_pending_fields_on_fiber(self): # Only check segment.rs (Fiber definition) matches = [m for m in matches if "segment.rs" in m] matches = _filter_non_test_non_comment(matches) - assert matches == [], f"pending_* fields on Fiber:\n" + "\n".join(matches) + assert matches == [], "pending_* fields on Fiber:\n" + "\n".join(matches) def test_no_interceptor_fields_on_fiber(self): """Fiber must not have interceptor_* fields.""" matches = _grep_rust(r"interceptor_", dirs=[VM_CORE_SRC]) matches = [m for m in matches if "segment.rs" in m] matches = _filter_non_test_non_comment(matches) - assert matches == [], f"interceptor_* fields on Fiber:\n" + "\n".join(matches) + assert matches == [], "interceptor_* fields on Fiber:\n" + "\n".join(matches) diff --git a/tests/test_await_concurrency.py b/tests/test_await_concurrency.py index 3ba0aee78..54057de6f 100644 --- a/tests/test_await_concurrency.py +++ b/tests/test_await_concurrency.py @@ -8,25 +8,18 @@ import asyncio import time -from typing import Any - -import pytest from doeff import ( AcquireSemaphore, + Await, CreateSemaphore, Gather, ReleaseSemaphore, Spawn, - default_handlers, do, - run, ) -from doeff import Await - - -from doeff import cache, WithHandler from tests._run_helpers import run_with_defaults + # REMOVED: from doeff_core_effects.handlers import sqlite_cache_handler diff --git a/tests/test_await_http_memory.py b/tests/test_await_http_memory.py index 7f731cb0e..97e89103e 100644 --- a/tests/test_await_http_memory.py +++ b/tests/test_await_http_memory.py @@ -15,23 +15,21 @@ import asyncio import resource import signal -import time from typing import Any -import pytest - from doeff import ( AcquireSemaphore, + Await, CreateSemaphore, Gather, ReleaseSemaphore, Spawn, WithHandler, do, - run, - Await, - slog,) + slog, +) from tests._run_helpers import run_with_defaults + # REMOVED: from doeff_core_effects.handlers import sqlite_cache_handler @@ -105,7 +103,7 @@ def _run_test(factory, n: int, conc: int = 40): signal.alarm(TIMEOUT_SECONDS) try: prog = WithHandler( - sqlite_cache_handler(None), + sqlite_cache_handler(None), # noqa: F821 - legacy removed API reference is intentionally preserved _spawn_gather(factory, n, conc), ) rss_before = _rss_mb() diff --git a/tests/test_cache_memory_bisect.py b/tests/test_cache_memory_bisect.py index 51c8be29f..a698e308a 100644 --- a/tests/test_cache_memory_bisect.py +++ b/tests/test_cache_memory_bisect.py @@ -6,22 +6,20 @@ from __future__ import annotations import asyncio -import os import signal from typing import Any -import pytest +from doeff_core_effects.cache import FrozenDict +from doeff_vm import GetExecutionContext from doeff import ( + Await, Gather, Spawn, + Try, do, slog, ) -from doeff import Await -from doeff_vm import GetExecutionContext -from doeff import Try -from doeff_core_effects.cache import FrozenDict from tests._run_helpers import run_with_defaults TIMEOUT_SECONDS = 60 @@ -128,9 +126,9 @@ def compute(key): yield Await(asyncio.sleep(0.001)) return f"done-{i}" - context = yield GetExecutionContext() + yield GetExecutionContext() key = yield build_key() - miss = yield try_get(key) + yield try_get(key) return (yield compute(key)) diff --git a/tests/test_cli_env_merge_with_program.py b/tests/test_cli_env_merge_with_program.py index cb399baa5..b4317e38a 100644 --- a/tests/test_cli_env_merge_with_program.py +++ b/tests/test_cli_env_merge_with_program.py @@ -9,10 +9,9 @@ Actual: run(env) fails with "unhandled effect: Local". """ -from doeff import Pure, do, run, WithHandler -from doeff_core_effects.effects import Local from doeff_core_effects.handlers import local_handler +from doeff import Pure, WithHandler, run from doeff.cli.discovery import StandardEnvMerger diff --git a/tests/test_core_effects.py b/tests/test_core_effects.py index e218f5ada..ece3183fd 100644 --- a/tests/test_core_effects.py +++ b/tests/test_core_effects.py @@ -1,8 +1,10 @@ """Tests for core effects — Ask, Get, Put, Tell.""" -from doeff import do, run as doeff_run, WithHandler, Pure -from doeff_core_effects.effects import Ask, Get, Put, Tell, Try, Slog -from doeff_core_effects.handlers import reader, state, writer, try_handler, slog_handler +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 run as doeff_run class TestReader: @@ -124,7 +126,7 @@ def body(): from doeff_vm import Ok result = doeff_run(WithHandler(try_handler, body())) - assert isinstance(result, Ok.__class__) or (hasattr(result, 'is_ok') and result.is_ok()) + assert isinstance(result, Ok.__class__) or (hasattr(result, "is_ok") and result.is_ok()) assert result.value == 42 def test_try_failure(self): @@ -138,9 +140,9 @@ def body(): result = yield Try(failing()) return result - from doeff_vm import Err result = doeff_run(WithHandler(try_handler, body())) - assert hasattr(result, 'is_err') and result.is_err() + assert hasattr(result, "is_err") + assert result.is_err() assert isinstance(result.error, ValueError) def test_try_does_not_propagate(self): @@ -152,7 +154,7 @@ def failing(): @do def body(): - result = yield Try(failing()) + yield Try(failing()) return "safe" assert doeff_run(WithHandler(try_handler, body())) == "safe" @@ -179,6 +181,7 @@ class TestAwait: def test_await_coroutine(self): """Await bridges async coroutines into doeff.""" import asyncio + from doeff_core_effects import Await, await_handler from doeff_core_effects.scheduler import scheduled @@ -197,6 +200,7 @@ def body(): def test_await_multiple(self): """Multiple Awaits in sequence.""" import asyncio + from doeff_core_effects import Await, await_handler from doeff_core_effects.scheduler import scheduled @@ -217,8 +221,9 @@ def test_await_100_concurrent_tasks(self): """100 spawned tasks each awaiting 100ms sleep — must finish in <2s.""" import asyncio import time + from doeff_core_effects import Await, await_handler - from doeff_core_effects.scheduler import scheduled, Spawn, Gather + from doeff_core_effects.scheduler import Gather, Spawn, scheduled async def fetch(x): await asyncio.sleep(0.1) @@ -241,7 +246,7 @@ def body(): result = doeff_run(scheduled(body())) elapsed = time.time() - start assert result == list(range(100)) - # 100 tasks × 0.1s sleep = should be ~0.1s if concurrent, not 10s + # 100 tasks x 0.1s sleep = should be ~0.1s if concurrent, not 10s assert elapsed < 2.0, f"took {elapsed:.1f}s — not concurrent!" diff --git a/tests/test_deep_handler.py b/tests/test_deep_handler.py index d78c47f2a..49e249366 100644 --- a/tests/test_deep_handler.py +++ b/tests/test_deep_handler.py @@ -6,11 +6,12 @@ This is the standard "deep handler" semantics from algebraic effects literature (OCaml 5, Koka, Effekt all use deep handlers by default). """ -from doeff import do, run, WithHandler, Resume, Transfer, Pass from doeff_core_effects import Ask from doeff_core_effects.handlers import reader from doeff_vm import EffectBase +from doeff import Pass, Resume, Transfer, WithHandler, do, run + def test_reader_handles_multiple_asks(): """reader handler must handle ALL Ask effects, not just the first one.""" diff --git a/tests/test_deftest_macro.py b/tests/test_deftest_macro.py index 43f0d8f57..0f1b3cabb 100644 --- a/tests/test_deftest_macro.py +++ b/tests/test_deftest_macro.py @@ -13,18 +13,13 @@ import importlib import sys import textwrap - -import pytest - -import hy -import hy.compiler -import hy.reader +from dataclasses import dataclass import doeff_hy # noqa — registers extensions -from doeff import DoExpr, WithHandler, Resume, Pass, run, do as _doeff_do -from doeff import EffectBase -from dataclasses import dataclass +import pytest +from doeff import EffectBase, Pass, Resume, WithHandler, run +from doeff import do as _doeff_do # --------------------------------------------------------------------------- # Test effect + handler for use in deftest tests @@ -40,7 +35,7 @@ def stub_handler(): def _handler(effect, k): if isinstance(effect, GetValue): values = {"price": 100.0, "name": "TestCo"} - yield Resume(k, values.get(effect.key, None)) + yield Resume(k, values.get(effect.key)) else: yield Pass(effect, k) return _handler diff --git a/tests/test_do_bang_setv.py b/tests/test_do_bang_setv.py index 5d9fd85d2..06c4303a5 100644 --- a/tests/test_do_bang_setv.py +++ b/tests/test_do_bang_setv.py @@ -6,12 +6,12 @@ """ import hy # noqa: F401 - -from doeff import WithHandler, do, run from doeff_core_effects import slog from doeff_core_effects.handlers import lazy_ask, writer from doeff_core_effects.scheduler import scheduled +from doeff import WithHandler, do, run + def _run_program(program): composed = WithHandler( diff --git a/tests/test_error_propagation.py b/tests/test_error_propagation.py index 654550984..a8f573e7b 100644 --- a/tests/test_error_propagation.py +++ b/tests/test_error_propagation.py @@ -1,10 +1,12 @@ """Tests for error propagation — Python exceptions flow through the VM correctly.""" import pytest -from doeff import do, run as doeff_run, WithHandler -from doeff.program import Resume, Pass, WithHandler +from doeff_core_effects.scheduler import Gather, Spawn, Wait, scheduled from doeff_vm import EffectBase -from doeff_core_effects.scheduler import scheduled, Spawn, Wait, Gather, TaskCancelledError + +from doeff import do +from doeff import run as doeff_run +from doeff.program import Resume, WithHandler class TestDirectErrors: diff --git a/tests/test_error_traceback_consistency.py b/tests/test_error_traceback_consistency.py index 7b59edb88..864fac339 100644 --- a/tests/test_error_traceback_consistency.py +++ b/tests/test_error_traceback_consistency.py @@ -3,9 +3,10 @@ Tests cover all user-facing error paths to ensure consistent visualization. """ import pytest -from doeff import do, run, WithHandler, Resume, Pass from doeff_vm import EffectBase +from doeff import Pass, WithHandler, do, run + class Unhandled(EffectBase): def __repr__(self): @@ -123,6 +124,6 @@ def prog(): tb = getattr(exc_info.value, "__doeff_traceback__", None) assert tb is not None # Should have handler chain entries - handler_entries = [e for e in tb if isinstance(e, list) and len(e) >= 2 and e[0] == "handler"] + [e for e in tb if isinstance(e, list) and len(e) >= 2 and e[0] == "handler"] # At minimum, should have frame entries assert_has_doeff_traceback(exc_info.value) diff --git a/tests/test_execution_scope.py b/tests/test_execution_scope.py index 195b23efe..3a5f97bcd 100644 --- a/tests/test_execution_scope.py +++ b/tests/test_execution_scope.py @@ -3,6 +3,8 @@ from dataclasses import dataclass from typing import Any +from doeff_core_effects.effects import EffectBase + from doeff import ( Effect, EffectGenerator, @@ -13,7 +15,6 @@ WithHandler, do, ) -from doeff_core_effects.effects import EffectBase from tests._run_helpers import run_with_defaults diff --git a/tests/test_finally_doctrl.py b/tests/test_finally_doctrl.py index 87443915e..16086d095 100644 --- a/tests/test_finally_doctrl.py +++ b/tests/test_finally_doctrl.py @@ -1,6 +1,6 @@ from __future__ import annotations -from doeff import Get, Pure, Put, Try, do +from doeff import Get, Put, Try, do from tests._run_helpers import run_with_defaults diff --git a/tests/test_get_handlers_defk.py b/tests/test_get_handlers_defk.py index e4918cbc2..dd110fd62 100644 --- a/tests/test_get_handlers_defk.py +++ b/tests/test_get_handlers_defk.py @@ -11,11 +11,17 @@ """ from __future__ import annotations -import pytest +from doeff_core_effects.handlers import ( + await_handler, + slog_handler, + state, + try_handler, + writer, +) +from doeff_core_effects.scheduler import Spawn, Wait, scheduled +from doeff_vm import EffectBase as VMEffectBase from doeff import ( - EffectBase, - EffectGenerator, Pass, Resume, WithHandler, @@ -23,17 +29,6 @@ run, ) from doeff.handler_utils import get_inner_handlers -from doeff_core_effects import slog -from doeff_core_effects.handlers import ( - await_handler, - slog_handler, - state, - try_handler, - writer, -) -from doeff_core_effects.scheduler import Spawn, Wait, Gather, scheduled -from doeff_vm import EffectBase as VMEffectBase, Ok, Err - # --- Effects --- @@ -44,7 +39,6 @@ def __init__(self, key): class MarkerEffect(VMEffectBase): """Marker effect to test handler visibility.""" - pass # --- Handlers --- diff --git a/tests/test_handle_macro.py b/tests/test_handle_macro.py index c7f2975da..a993a5d48 100644 --- a/tests/test_handle_macro.py +++ b/tests/test_handle_macro.py @@ -7,24 +7,21 @@ - defhandler inside defn factory does not leak yield (#387) """ -import importlib import inspect import sys -import textwrap +from dataclasses import dataclass -import pytest +import doeff_hy # noqa — registers extensions import hy import hy.compiler -import hy.reader import hy.macros - -import doeff_hy # noqa — registers extensions -from doeff import DoExpr, WithHandler, Resume, Transfer, Pass, run, do as _doeff_do -from doeff import EffectBase -from doeff_core_effects.scheduler import scheduled +import hy.reader +import pytest from doeff_core_effects.handlers import await_handler -from dataclasses import dataclass +from doeff_core_effects.scheduler import scheduled +from doeff import EffectBase, Pass, Resume, Transfer, WithHandler, run +from doeff import do as _doeff_do # --------------------------------------------------------------------------- # Test effects diff --git a/tests/test_handler_chain_traversal.py b/tests/test_handler_chain_traversal.py index 9c2aa84cb..89b639505 100644 --- a/tests/test_handler_chain_traversal.py +++ b/tests/test_handler_chain_traversal.py @@ -7,19 +7,18 @@ Works with 3 handlers, fails with 29. """ -from doeff import do, WithHandler, run -from doeff_vm import EffectBase, Resume, Pass from doeff_core_effects.scheduler import scheduled +from doeff_vm import EffectBase, Pass, Resume + +from doeff import WithHandler, do, run class EffectA(EffectBase): """Handled by handler at position 9.""" - pass class EffectB(EffectBase): """Emitted by handler_a, should be caught by handler at position 18.""" - pass @do diff --git a/tests/test_handler_exception_catchable.py b/tests/test_handler_exception_catchable.py index 723cb9133..2dc1fb6d7 100644 --- a/tests/test_handler_exception_catchable.py +++ b/tests/test_handler_exception_catchable.py @@ -17,19 +17,17 @@ from dataclasses import dataclass -import pytest +from doeff_core_effects.handlers import try_handler +from doeff_vm import Err from doeff import ( EffectBase, Pass, - Resume, Try, WithHandler, do, run, ) -from doeff_core_effects.handlers import try_handler -from doeff_vm import Err, Ok @dataclass(frozen=True, kw_only=True) diff --git a/tests/test_handler_nested_do.py b/tests/test_handler_nested_do.py index 41436e1f8..1dc6b409b 100644 --- a/tests/test_handler_nested_do.py +++ b/tests/test_handler_nested_do.py @@ -4,9 +4,10 @@ do those effects reach the outer handler stack? """ -from doeff import do, WithHandler, run -from doeff_vm import EffectBase, Resume, Pass from doeff_core_effects.scheduler import scheduled +from doeff_vm import EffectBase, Pass, Resume + +from doeff import WithHandler, do, run class EffectA(EffectBase): diff --git a/tests/test_intercept.py b/tests/test_intercept.py index e7bf28fc5..707e944a3 100644 --- a/tests/test_intercept.py +++ b/tests/test_intercept.py @@ -1,8 +1,10 @@ """Tests for WithObserve — effect observation.""" -from doeff import do, run as doeff_run, WithHandler, WithObserve -from doeff_vm import EffectBase, Callable -from doeff.program import Resume, Pass +from doeff_vm import Callable, EffectBase + +from doeff import WithHandler, WithObserve, do +from doeff import run as doeff_run +from doeff.program import Resume class TestObserve: diff --git a/tests/test_llm_multi_provider_handlers.py b/tests/test_llm_multi_provider_handlers.py index f07508e16..9de00dc16 100644 --- a/tests/test_llm_multi_provider_handlers.py +++ b/tests/test_llm_multi_provider_handlers.py @@ -5,11 +5,11 @@ from typing import Any import pytest - from doeff_gemini.handlers import gemini_mock_handler from doeff_llm.effects import LLMChat, LLMStructuredQuery from doeff_openrouter.handlers import MockOpenRouterRuntime, openrouter_mock_handler from pydantic import BaseModel + from tests._run_helpers import run_with_defaults try: @@ -24,7 +24,7 @@ allow_module_level=True, ) -from doeff import Effect, EffectGenerator, WithHandler, default_handlers, do, run +from doeff import Effect, EffectGenerator, WithHandler, do class AnalysisResult(BaseModel): diff --git a/tests/test_macro_self_contained.py b/tests/test_macro_self_contained.py index f1c9e067b..0025ed1a2 100644 --- a/tests/test_macro_self_contained.py +++ b/tests/test_macro_self_contained.py @@ -7,18 +7,16 @@ import sys import types +from dataclasses import dataclass -import pytest +import doeff_hy # noqa — registers extensions import hy import hy.macros - -import doeff_hy # noqa — registers extensions -from doeff import run, WithHandler -from doeff import EffectBase from doeff_core_effects import Ask from doeff_core_effects.handlers import await_handler, lazy_ask from doeff_core_effects.scheduler import scheduled -from dataclasses import dataclass + +from doeff import EffectBase, WithHandler, run @dataclass(frozen=True) diff --git a/tests/test_memo_rewriter_spawn_continuation.py b/tests/test_memo_rewriter_spawn_continuation.py index cf08daa64..546bd7499 100644 --- a/tests/test_memo_rewriter_spawn_continuation.py +++ b/tests/test_memo_rewriter_spawn_continuation.py @@ -14,20 +14,7 @@ from dataclasses import dataclass -from doeff import ( - EffectBase, - EffectGenerator, - Pass, - Resume, - WithHandler, - do, - run, -) -from doeff_core_effects import slog, Try -from doeff_core_effects.memo_handlers import ( - in_memory_memo_handler, - make_memo_rewriter, -) +from doeff_core_effects import Try, slog from doeff_core_effects.handlers import ( await_handler, slog_handler, @@ -36,10 +23,16 @@ writer, ) from doeff_core_effects.scheduler import Spawn, Wait, scheduled -from doeff_time import GetTime -from doeff_time.handlers import sim_time_handler -from doeff_traverse.handlers import parallel, fail_handler +from doeff import ( + EffectBase, + EffectGenerator, + Pass, + Resume, + WithHandler, + do, + run, +) # --- Custom effect (analogous to HistoricalPriceQuery) --- @@ -144,7 +137,7 @@ def prog() -> EffectGenerator[dict]: # Additional import for nested handler tests -from dataclasses import dataclass as _dc_import_check # noqa: F401 +from dataclasses import dataclass as _dc_import_check # noqa: E402, F401 - late import fixture class TestMemoRewriterWithSimTime: diff --git a/tests/test_new_vm.py b/tests/test_new_vm.py index 8929511a9..c82708251 100644 --- a/tests/test_new_vm.py +++ b/tests/test_new_vm.py @@ -5,8 +5,10 @@ """ import pytest -from doeff_vm import PyVM, K, Callable, EffectBase, IRStream -from doeff import Pass, do, run as doeff_run, WithHandler +from doeff_vm import EffectBase, PyVM + +from doeff import Pass, WithHandler, do +from doeff import run as doeff_run from doeff.program import program @@ -82,7 +84,7 @@ def test_pure_none(self, vm): assert vm.run(Pure(None)) is None def test_pure_bool(self, vm): - assert vm.run(Pure(True)) == True + assert vm.run(Pure(True)) def test_run_rejects_raw_generator(self, vm): """Raw generators must not be accepted — use DoExpr.""" @@ -268,7 +270,7 @@ def body(): yield Perform(None) with pytest.raises(RuntimeError): - doeff_run(WithHandler(lambda e, k: Pure(None), program(body))) + doeff_run(WithHandler(lambda _e, _k: Pure(None), program(body))) def test_no_handler_error(self): """Performing without a handler raises an error.""" @@ -285,7 +287,7 @@ def body(): # To test no-handler, we need a body that performs inside run_with_handler # but with a handler that doesn't exist... or no handler at all. # For now, just test that the error is raised properly. - pass # TODO: once Pass is tested from Python + # TODO: once Pass is tested from Python # --------------------------------------------------------------------------- diff --git a/tests/test_pass_dispatch_context_regression.py b/tests/test_pass_dispatch_context_regression.py index b449982fc..c8b00437e 100644 --- a/tests/test_pass_dispatch_context_regression.py +++ b/tests/test_pass_dispatch_context_regression.py @@ -12,15 +12,10 @@ from __future__ import annotations -import pytest - import doeff -from doeff import WithHandler, do -from doeff import EffectBase + # REMOVED: from doeff_core_effects.cache import in_memory_cache_handler, memo_rewriters -from doeff import default_handlers, run -from doeff import EffectGenerator -from tests._run_helpers import run_with_defaults +from doeff import EffectBase, WithHandler, do class EffectA(EffectBase): diff --git a/tests/test_pass_then_typed_handler.py b/tests/test_pass_then_typed_handler.py index 96719aa3c..9a4008ccf 100644 --- a/tests/test_pass_then_typed_handler.py +++ b/tests/test_pass_then_typed_handler.py @@ -18,7 +18,7 @@ from dataclasses import dataclass import doeff_vm -import pytest +from doeff_vm import WithHandler from doeff import ( Effect, @@ -27,11 +27,9 @@ WriterTellEffect, do, run, + slog, ) -from doeff import slog -from doeff_vm import WithHandler -from tests._run_helpers import run_with_defaults, wrap_with_defaults - +from tests._run_helpers import run_with_defaults # -- Effects ----------------------------------------------------------------- @@ -124,14 +122,14 @@ def _mediagen_stack(program): Effect path for slog: program → WithIntercept → memo_rewriter(Pass) → replace_handler """ - intercepted = WithIntercept( + intercepted = WithIntercept( # noqa: F821 - legacy removed API reference is intentionally preserved slog_interceptor, program, types=(WriterTellEffect,), mode="include" ) wrapped = intercepted # memo_rewriter first (innermost), replace_handler wraps it (outer) for h in [memo_rewriter, replace_handler]: wrapped = WithHandler(h, wrapped) - return run(wrapped, handlers=[*default_handlers()]) + return run(wrapped, handlers=[*default_handlers()]) # noqa: F821 - legacy removed API reference is intentionally preserved def _no_intercept_stack(program): diff --git a/tests/test_pass_traceback.py b/tests/test_pass_traceback.py index d2978026f..ab800f98a 100644 --- a/tests/test_pass_traceback.py +++ b/tests/test_pass_traceback.py @@ -7,14 +7,14 @@ - Which effect was not handled (type name + repr) - Where in user code the effect was performed (file, line, function) """ -from doeff import do, run, WithHandler, Resume, Pass -from doeff_vm import EffectBase import pytest +from doeff_vm import EffectBase + +from doeff import Pass, Resume, WithHandler, do, run class Handled(EffectBase): """An effect that WILL be handled.""" - pass class Unhandled(EffectBase): diff --git a/tests/test_pyclass_pickle.py b/tests/test_pyclass_pickle.py index 53ae33d56..bdd3eacbf 100644 --- a/tests/test_pyclass_pickle.py +++ b/tests/test_pyclass_pickle.py @@ -9,7 +9,6 @@ import cloudpickle from doeff_vm.doeff_vm import Err, Ok - # --------------------------------------------------------------------------- # Ok / Err (core Result types — used by cache) # --------------------------------------------------------------------------- diff --git a/tests/test_run_program_services.py b/tests/test_run_program_services.py index f89211cdd..432a4c88e 100644 --- a/tests/test_run_program_services.py +++ b/tests/test_run_program_services.py @@ -1,10 +1,7 @@ from __future__ import annotations -import importlib -import logging from pathlib import Path - ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_scheduled_handler_isolation.py b/tests/test_scheduled_handler_isolation.py index d3a1328ba..005f1ae13 100644 --- a/tests/test_scheduled_handler_isolation.py +++ b/tests/test_scheduled_handler_isolation.py @@ -5,10 +5,11 @@ whose handlers emit effects. """ -from doeff import do, Ask, WithHandler, Local, run -from doeff_vm import EffectBase, Resume, Pass -from doeff_core_effects.scheduler import scheduled from doeff_core_effects.handlers import lazy_ask +from doeff_core_effects.scheduler import scheduled +from doeff_vm import EffectBase, Pass, Resume + +from doeff import Ask, WithHandler, do, run class Transcribe(EffectBase): @@ -19,11 +20,9 @@ class Resolve(EffectBase): class ResolveHandlerKey: """Ask key for resolve handler lookup.""" - pass class TranscribeHandlerKey: """Ask key for transcribe handler lookup.""" - pass @do diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index be346e164..6211f6e4d 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -432,7 +432,7 @@ def body(): class TestPromise: def test_create_and_complete_promise(self): """Internal promise: create, complete, wait.""" - from doeff_core_effects.scheduler import CreatePromise, CompletePromise + from doeff_core_effects.scheduler import CompletePromise, CreatePromise @do def body(): @@ -444,7 +444,7 @@ def body(): def test_promise_across_tasks(self): """One task creates promise, another waits, first completes.""" - from doeff_core_effects.scheduler import CreatePromise, CompletePromise + from doeff_core_effects.scheduler import CompletePromise, CreatePromise @do def body(): @@ -504,6 +504,7 @@ def test_100_threads_concurrent(self): """100 tasks each sleeping 0.1s in threads. Must finish in <2s, not 10s.""" import threading import time + from doeff_core_effects.scheduler import CreateExternalPromise @do @@ -539,7 +540,7 @@ def body(): class TestCancel: def test_cancel_blocked_task(self): """Cancel a task that's blocked waiting on a promise.""" - from doeff_core_effects.scheduler import Cancel, TaskCancelledError, CreateExternalPromise + from doeff_core_effects.scheduler import Cancel, CreateExternalPromise, TaskCancelledError @do def body(): @@ -646,7 +647,7 @@ def body(): class TestSemaphore: def test_binary_semaphore_mutual_exclusion(self): """Binary semaphore (permits=1) ensures mutual exclusion.""" - from doeff_core_effects.scheduler import CreateSemaphore, AcquireSemaphore, ReleaseSemaphore + from doeff_core_effects.scheduler import AcquireSemaphore, CreateSemaphore, ReleaseSemaphore log = [] @@ -669,12 +670,11 @@ def body(): assert result == ["A", "B"] # Mutual exclusion: no interleaving of enter/exit # Either [A_enter, A_exit, B_enter, B_exit] or [B_enter, B_exit, A_enter, A_exit] - assert (log == ["A_enter", "A_exit", "B_enter", "B_exit"] or - log == ["B_enter", "B_exit", "A_enter", "A_exit"]) + assert (log in (["A_enter", "A_exit", "B_enter", "B_exit"], ["B_enter", "B_exit", "A_enter", "A_exit"])) def test_counting_semaphore(self): """Counting semaphore allows N concurrent accessors.""" - from doeff_core_effects.scheduler import CreateSemaphore, AcquireSemaphore, ReleaseSemaphore + from doeff_core_effects.scheduler import AcquireSemaphore, CreateSemaphore, ReleaseSemaphore @do def worker(sem, i): diff --git a/tests/test_segment_owned_state.py b/tests/test_segment_owned_state.py index fae7973a4..288a54e26 100644 --- a/tests/test_segment_owned_state.py +++ b/tests/test_segment_owned_state.py @@ -3,17 +3,17 @@ from dataclasses import dataclass from typing import Any +from doeff_core_effects.effects import EffectBase + from doeff import ( Effect, EffectGenerator, Pass, - Pure, Resume, Try, WithHandler, do, - ) -from doeff_core_effects.effects import EffectBase +) from tests._run_helpers import run_with_defaults diff --git a/tests/test_spawn_with_handlers.py b/tests/test_spawn_with_handlers.py index 398c9fe86..046f34443 100644 --- a/tests/test_spawn_with_handlers.py +++ b/tests/test_spawn_with_handlers.py @@ -7,14 +7,18 @@ "generator yielded non-DoExpr: ."` when a spawned task performs effects that require handlers. """ -from doeff import do, run, WithHandler, Resume, Pass -from doeff_core_effects import Ask, Try, slog, Tell +from doeff_core_effects import Ask, Tell, Try from doeff_core_effects.handlers import ( - reader, state, writer, try_handler, slog_handler, await_handler, + reader, + slog_handler, + state, + try_handler, + writer, ) -from doeff_core_effects.scheduler import Spawn, Wait, Gather, scheduled +from doeff_core_effects.scheduler import Gather, Spawn, Wait, scheduled from doeff_vm import EffectBase, Ok +from doeff import Pass, Resume, WithHandler, do, run # --- Custom effect + handler (mimics LLMStructuredQuery + openai_handler) --- diff --git a/tests/test_traceback_dedup_frames.py b/tests/test_traceback_dedup_frames.py index c4ece7453..e8b880366 100644 --- a/tests/test_traceback_dedup_frames.py +++ b/tests/test_traceback_dedup_frames.py @@ -2,13 +2,13 @@ For non-passthrough handlers where Resume IS genuinely needed, consecutive identical frames are compressed in collect_rich_context_from with a count -field, and format_default renders [×N]. +field, and format_default renders a multiplication-sign count marker. """ from doeff.traceback import format_default def test_format_default_renders_repeat_count(): - """format_default should render [×N] for frames with count > 1.""" + """format_default should render a count marker for frames with count > 1.""" exc = KeyError("test") exc.__doeff_traceback__ = [ ["frame", "my_handler", "/fake/handler.py", 42, 15], @@ -16,14 +16,14 @@ def test_format_default_renders_repeat_count(): ] rendered = format_default(exc) assert rendered is not None - assert "[×15]" in rendered + assert "[×15]" in rendered # noqa: RUF001 - test fixture intentionally matches the literal rendered glyph for line in rendered.split("\n"): if "my_body" in line: - assert "[×" not in line + assert "[×" not in line # noqa: RUF001 - test fixture intentionally matches the literal rendered glyph def test_format_default_omits_count_for_single(): - """Frames with count=1 or no count field should not show [×1].""" + """Frames with count=1 or no count field should not show a count marker.""" exc = KeyError("test") exc.__doeff_traceback__ = [ ["frame", "fn_a", "/fake/a.py", 1, 1], @@ -31,4 +31,4 @@ def test_format_default_omits_count_for_single(): ] rendered = format_default(exc) assert rendered is not None - assert "[×" not in rendered + assert "[×" not in rendered # noqa: RUF001 - test fixture intentionally matches the literal rendered glyph diff --git a/tests/test_try_deep_in_handler_body.py b/tests/test_try_deep_in_handler_body.py index 58da646c1..3c2d13f66 100644 --- a/tests/test_try_deep_in_handler_body.py +++ b/tests/test_try_deep_in_handler_body.py @@ -14,10 +14,11 @@ → yield get_openai_client() # @do → yield Try(try_ask_client()) # Try 3 levels deep """ -from doeff import do, run, WithHandler, Resume, Pass -from doeff_core_effects import Ask, Try, Get, Put, Tell -from doeff_core_effects.handlers import reader, try_handler, state, writer -from doeff_vm import EffectBase, Ok, Err +from doeff_core_effects import Ask, Get, Tell, Try +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 class CustomQuery(EffectBase): @@ -191,7 +192,7 @@ def attempt(): return f"success on attempt {call_count}" last_error = None - for i in range(5): + for _i in range(5): safe = yield Try(attempt()) if safe.is_ok(): return (yield Resume(k, safe.value)) diff --git a/tests/test_try_handler.py b/tests/test_try_handler.py index e45122869..cb66b8abe 100644 --- a/tests/test_try_handler.py +++ b/tests/test_try_handler.py @@ -3,10 +3,11 @@ Try(program) should be caught by try_handler and return Ok/Err. Currently fails because Try does not satisfy the VM effect protocol. """ -from doeff import do, run, WithHandler from doeff_core_effects import Try from doeff_core_effects.handlers import try_handler -from doeff_vm import Ok, Err +from doeff_vm import Err, Ok + +from doeff import WithHandler, do, run def test_try_catches_error(): diff --git a/tests/test_try_handler_scope.py b/tests/test_try_handler_scope.py index f39db6657..b6433bec2 100644 --- a/tests/test_try_handler_scope.py +++ b/tests/test_try_handler_scope.py @@ -7,11 +7,12 @@ Bug reproduction: if a custom effect handler is installed INNER to try_handler, effects performed inside Try(program) cannot reach that inner handler. """ -from doeff import do, run, WithHandler, Resume, Pass from doeff_core_effects import Ask, Try, slog -from doeff_core_effects.handlers import reader, try_handler, slog_handler, writer +from doeff_core_effects.handlers import reader, try_handler, writer from doeff_vm import EffectBase +from doeff import Pass, Resume, WithHandler, do, run + class CustomEffect(EffectBase): """A custom effect for testing handler chain scope.""" diff --git a/tests/test_try_in_handler_body.py b/tests/test_try_in_handler_body.py index c00a99a1b..f2f4d296c 100644 --- a/tests/test_try_in_handler_body.py +++ b/tests/test_try_in_handler_body.py @@ -6,10 +6,11 @@ Bug: openai_production_handler yields Try(try_ask_client()) inside its handler body, but Try is not caught by try_handler (which is outer). """ -from doeff import do, run, WithHandler, Resume, Pass from doeff_core_effects import Ask, Try from doeff_core_effects.handlers import reader, try_handler -from doeff_vm import EffectBase, Ok, Err +from doeff_vm import EffectBase + +from doeff import Pass, Resume, WithHandler, do, run class LLMQuery(EffectBase): diff --git a/tests/test_with_handler_type_filter_regression.py b/tests/test_with_handler_type_filter_regression.py index 97e462499..8aac836d1 100644 --- a/tests/test_with_handler_type_filter_regression.py +++ b/tests/test_with_handler_type_filter_regression.py @@ -13,10 +13,9 @@ from dataclasses import dataclass import doeff_vm -import pytest +from doeff_vm import WithHandler from doeff import Effect, EffectBase, EffectGenerator, do -from doeff_vm import WithHandler from tests._run_helpers import run_with_defaults diff --git a/tests/test_with_observe_visibility.py b/tests/test_with_observe_visibility.py index 7a033ff9e..ab95d2abb 100644 --- a/tests/test_with_observe_visibility.py +++ b/tests/test_with_observe_visibility.py @@ -7,16 +7,17 @@ Expected: WithObserve observes every effect that passes through the VM, regardless of whether it's yielded from the main program or from a handler body. """ -from doeff import do, run, WithHandler, WithObserve, Resume, Pass -from doeff_core_effects import Ask, Try, slog, Tell +from doeff_core_effects import Tell, slog from doeff_core_effects.handlers import ( - reader, state, writer, try_handler, slog_handler, + slog_handler, + state, + try_handler, + writer, ) -from doeff_core_effects.memo_effects import MemoExists, MemoGet, MemoPut, MemoExistsEffect, MemoGetEffect, MemoPutEffect -from doeff_core_effects.memo_handlers import ( - memo_handler, make_memo_rewriter, in_memory_memo_handler, -) -from doeff_vm import EffectBase, Callable as VMCallable +from doeff_vm import Callable as VMCallable +from doeff_vm import EffectBase + +from doeff import Pass, Resume, WithHandler, WithObserve, do, run class CustomQuery(EffectBase): @@ -66,7 +67,7 @@ def test_observe_sees_handler_body_effects(): observed = [] def observer(effect): - if hasattr(effect, 'msg'): + if hasattr(effect, "msg"): observed.append(f"{type(effect).__name__}:{effect.msg}") else: observed.append(type(effect).__name__) diff --git a/tests/test_withhandler_shim_deprecation.py b/tests/test_withhandler_shim_deprecation.py index 411a71490..2c3b47016 100644 --- a/tests/test_withhandler_shim_deprecation.py +++ b/tests/test_withhandler_shim_deprecation.py @@ -17,9 +17,7 @@ import warnings -import pytest - -from doeff import Pure, Resume, Pass, WithHandler, WithHandlerType, do, run +from doeff import Pass, Pure, WithHandler, WithHandlerType, do, run from doeff.program import _WithHandlerNode From cb449d5c511d86968daaa3497601f43f5d431947 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:57 +0900 Subject: [PATCH 02/35] Clean Ruff violations in doeff-agentic --- .../doeff-agentic/examples/01_hello_agent.py | 1 + .../examples/02_agent_with_status.py | 3 +- .../examples/03_sequential_agents.py | 3 +- .../examples/04_conditional_flow.py | 3 +- .../examples/05_human_in_loop.py | 3 +- .../examples/06_parallel_agents.py | 3 +- .../examples/07_pr_review_workflow.py | 2 +- .../examples/08_testing_with_mocks.py | 3 +- .../examples/test_event_logging.py | 2 +- .../src/doeff_agentic/__init__.py | 24 ++++++------ .../doeff-agentic/src/doeff_agentic/api.py | 4 +- .../doeff-agentic/src/doeff_agentic/cli.py | 2 +- .../src/doeff_agentic/event_log.py | 14 +++---- .../src/doeff_agentic/handler.py | 2 +- .../src/doeff_agentic/handlers/opencode.py | 38 +++++++++---------- .../src/doeff_agentic/handlers/production.py | 31 ++++++++------- .../src/doeff_agentic/handlers/testing.py | 3 +- .../src/doeff_agentic/handlers/tmux.py | 23 +++++------ .../src/doeff_agentic/opencode_handler.py | 2 +- .../doeff-agentic/src/doeff_agentic/state.py | 9 ++--- .../src/doeff_agentic/tmux_handler.py | 2 +- .../src/doeff_agentic/tui/app.py | 5 ++- .../src/doeff_agentic/tui/screens.py | 11 +++--- .../src/doeff_agentic/tui/widgets.py | 4 +- .../src/doeff_agentic/visual_interceptor.py | 9 ++--- packages/doeff-agentic/tests/test_effects.py | 2 +- .../tests/test_visual_interceptor.py | 4 +- 27 files changed, 107 insertions(+), 105 deletions(-) diff --git a/packages/doeff-agentic/examples/01_hello_agent.py b/packages/doeff-agentic/examples/01_hello_agent.py index 5fde7bc02..c455f4eec 100644 --- a/packages/doeff-agentic/examples/01_hello_agent.py +++ b/packages/doeff-agentic/examples/01_hello_agent.py @@ -47,6 +47,7 @@ def hello_agent(): if __name__ == "__main__": import asyncio + from doeff import WithHandler, async_run, default_handlers async def main(): diff --git a/packages/doeff-agentic/examples/02_agent_with_status.py b/packages/doeff-agentic/examples/02_agent_with_status.py index d73ddf72a..f6aa08899 100644 --- a/packages/doeff-agentic/examples/02_agent_with_status.py +++ b/packages/doeff-agentic/examples/02_agent_with_status.py @@ -15,6 +15,7 @@ doeff-agentic watch """ +from doeff.effects.writer import slog from doeff_agentic import ( AgenticCreateSession, AgenticGetMessages, @@ -25,7 +26,6 @@ from doeff_preset import preset_handlers from doeff import do -from doeff.effects.writer import slog def get_last_assistant_message(messages: list[AgenticMessage]) -> str: @@ -61,6 +61,7 @@ def agent_with_status(): if __name__ == "__main__": import asyncio + from doeff import WithHandler, async_run, default_handlers async def main(): diff --git a/packages/doeff-agentic/examples/03_sequential_agents.py b/packages/doeff-agentic/examples/03_sequential_agents.py index 3a48076ad..9db61f23e 100644 --- a/packages/doeff-agentic/examples/03_sequential_agents.py +++ b/packages/doeff-agentic/examples/03_sequential_agents.py @@ -11,6 +11,7 @@ uv run python examples/03_sequential_agents.py """ +from doeff.effects.writer import slog from doeff_agentic import ( AgenticCreateSession, AgenticGetMessages, @@ -21,7 +22,6 @@ from doeff_preset import preset_handlers from doeff import do -from doeff.effects.writer import slog def get_last_assistant_message(messages: list[AgenticMessage]) -> str: @@ -67,6 +67,7 @@ def research_and_summarize(topic: str): if __name__ == "__main__": import asyncio + from doeff import WithHandler, async_run, default_handlers async def main(): diff --git a/packages/doeff-agentic/examples/04_conditional_flow.py b/packages/doeff-agentic/examples/04_conditional_flow.py index 949213209..6aca25457 100644 --- a/packages/doeff-agentic/examples/04_conditional_flow.py +++ b/packages/doeff-agentic/examples/04_conditional_flow.py @@ -11,6 +11,7 @@ uv run python examples/04_conditional_flow.py """ +from doeff.effects.writer import slog from doeff_agentic import ( AgenticCreateSession, AgenticGetMessages, @@ -21,7 +22,6 @@ from doeff_preset import preset_handlers from doeff import do -from doeff.effects.writer import slog def get_last_assistant_message(messages: list[AgenticMessage]) -> str: @@ -79,6 +79,7 @@ def review_and_maybe_fix(code: str): if __name__ == "__main__": import asyncio + from doeff import WithHandler, async_run, default_handlers async def main(): diff --git a/packages/doeff-agentic/examples/05_human_in_loop.py b/packages/doeff-agentic/examples/05_human_in_loop.py index 3cbb3b64f..2e285e8de 100644 --- a/packages/doeff-agentic/examples/05_human_in_loop.py +++ b/packages/doeff-agentic/examples/05_human_in_loop.py @@ -20,6 +20,7 @@ import time +from doeff.effects.writer import slog from doeff_agentic import ( AgenticCreateSession, AgenticEndOfEvents, @@ -33,7 +34,6 @@ from doeff_preset import preset_handlers from doeff import do -from doeff.effects.writer import slog def get_last_assistant_message(messages: list[AgenticMessage]) -> str: @@ -163,6 +163,7 @@ def draft_with_approval(task: str): if __name__ == "__main__": import asyncio + from doeff import WithHandler, async_run, default_handlers async def main(): diff --git a/packages/doeff-agentic/examples/06_parallel_agents.py b/packages/doeff-agentic/examples/06_parallel_agents.py index f2fe99ae8..f2c4e2cd9 100644 --- a/packages/doeff-agentic/examples/06_parallel_agents.py +++ b/packages/doeff-agentic/examples/06_parallel_agents.py @@ -17,6 +17,7 @@ uv run python examples/06_parallel_agents.py """ +from doeff.effects.writer import slog from doeff_agentic import ( AgenticCreateSession, AgenticGetMessages, @@ -27,7 +28,6 @@ from doeff_preset import preset_handlers from doeff import Gather, Spawn, do -from doeff.effects.writer import slog def get_last_assistant_message(messages: list[AgenticMessage]) -> str: @@ -115,6 +115,7 @@ def multi_perspective_analysis(topic: str): if __name__ == "__main__": import asyncio + from doeff import WithHandler, async_run, default_handlers async def main(): diff --git a/packages/doeff-agentic/examples/07_pr_review_workflow.py b/packages/doeff-agentic/examples/07_pr_review_workflow.py index c5783411c..14c073620 100644 --- a/packages/doeff-agentic/examples/07_pr_review_workflow.py +++ b/packages/doeff-agentic/examples/07_pr_review_workflow.py @@ -16,6 +16,7 @@ import time +from doeff.effects.writer import slog from doeff_agentic import ( AgenticCreateSession, AgenticEndOfEvents, @@ -29,7 +30,6 @@ from doeff_preset import preset_handlers from doeff import do -from doeff.effects.writer import slog def get_last_assistant_message(messages: list[AgenticMessage]) -> str: diff --git a/packages/doeff-agentic/examples/08_testing_with_mocks.py b/packages/doeff-agentic/examples/08_testing_with_mocks.py index be6a04dde..5b6ea80c3 100644 --- a/packages/doeff-agentic/examples/08_testing_with_mocks.py +++ b/packages/doeff-agentic/examples/08_testing_with_mocks.py @@ -14,7 +14,6 @@ import sys -from doeff import WithHandler, default_handlers, do, run from doeff_agentic import ( AgenticCreateSession, AgenticGetMessages, @@ -23,6 +22,8 @@ ) from doeff_agentic.handlers.testing import MockAgenticHandler, mock_handlers +from doeff import WithHandler, default_handlers, do, run + @do def mock_conversation(): diff --git a/packages/doeff-agentic/examples/test_event_logging.py b/packages/doeff-agentic/examples/test_event_logging.py index dd09aac48..4a23b56eb 100644 --- a/packages/doeff-agentic/examples/test_event_logging.py +++ b/packages/doeff-agentic/examples/test_event_logging.py @@ -27,7 +27,7 @@ ) -def test_event_logging(): +def test_event_logging(): # noqa: PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged """Test the event logging system end-to-end.""" print("=" * 60) print("Testing Event Logging System") diff --git a/packages/doeff-agentic/src/doeff_agentic/__init__.py b/packages/doeff-agentic/src/doeff_agentic/__init__.py index 785e1c046..c49b7c364 100644 --- a/packages/doeff-agentic/src/doeff_agentic/__init__.py +++ b/packages/doeff-agentic/src/doeff_agentic/__init__.py @@ -127,16 +127,16 @@ def my_workflow(): AgenticUnsupportedOperationError, AgenticWorkflowNotFoundError, ) +from .handlers import ( + mock_handlers, + production_handlers, +) # OpenCode Handler (new - primary) from .opencode_handler import ( OpenCodeHandler, opencode_handler, ) -from .handlers import ( - mock_handlers, - production_handlers, -) # State management from .state import ( @@ -150,12 +150,6 @@ def my_workflow(): TmuxHandler, tmux_handler, ) - -# Visual Interceptor (for examples/debugging) -from .visual_interceptor import ( - VisualInterceptorConfig, - with_visual_logging, -) from .types import ( # Legacy types (deprecated) AgentConfig, @@ -180,6 +174,12 @@ def my_workflow(): WorkflowStatus, ) +# Visual Interceptor (for examples/debugging) +from .visual_interceptor import ( + VisualInterceptorConfig, + with_visual_logging, +) + # Legacy Handler (deprecated - requires doeff-agents) try: from .handler import ( @@ -263,6 +263,7 @@ def my_workflow(): # Tmux Handler (legacy fallback) "TmuxHandler", "UserInputTimeoutError", + "VisualInterceptorConfig", "WaitForStatus", "WaitForStatusEffect", "WaitForUserInput", @@ -278,10 +279,9 @@ def my_workflow(): "agentic_effectful_handlers", "generate_workflow_id", "get_default_state_dir", + "mock_handlers", "opencode_handler", "production_handlers", - "mock_handlers", "tmux_handler", - "VisualInterceptorConfig", "with_visual_logging", ] diff --git a/packages/doeff-agentic/src/doeff_agentic/api.py b/packages/doeff-agentic/src/doeff_agentic/api.py index be785d9ca..7f2443415 100644 --- a/packages/doeff-agentic/src/doeff_agentic/api.py +++ b/packages/doeff-agentic/src/doeff_agentic/api.py @@ -1,5 +1,3 @@ -from __future__ import annotations - """ High-level API for doeff-agentic. @@ -27,6 +25,8 @@ api.stop("a3f") """ +from __future__ import annotations + from collections.abc import Iterator from pathlib import Path from typing import Any diff --git a/packages/doeff-agentic/src/doeff_agentic/cli.py b/packages/doeff-agentic/src/doeff_agentic/cli.py index 219ce3d9d..f4672d082 100644 --- a/packages/doeff-agentic/src/doeff_agentic/cli.py +++ b/packages/doeff-agentic/src/doeff_agentic/cli.py @@ -279,7 +279,7 @@ def attach(ctx: click.Context, target: str) -> None: f"[yellow]Multiple agents in workflow {workflow.id}. Specify session:[/yellow]" ) for a in workflow.agents: - style = _status_color(a.status) + _status_color(a.status) console.print(f" doeff-agentic attach {workflow.id}:{a.name}") sys.exit(1) diff --git a/packages/doeff-agentic/src/doeff_agentic/event_log.py b/packages/doeff-agentic/src/doeff_agentic/event_log.py index fd1d7b039..19b35ce59 100644 --- a/packages/doeff-agentic/src/doeff_agentic/event_log.py +++ b/packages/doeff-agentic/src/doeff_agentic/event_log.py @@ -46,6 +46,7 @@ """ +import contextlib import json import os import tempfile @@ -88,10 +89,8 @@ def _atomic_write(path: Path, content: str) -> None: os.rename(temp_path, path) except Exception: os.close(fd) - try: + with contextlib.suppress(OSError): os.unlink(temp_path) - except OSError: - pass raise @@ -484,8 +483,8 @@ def _read_jsonl(self, path: Path) -> list[EventLogEntry]: return [] entries = [] with open(path, encoding="utf-8") as f: - for line in f: - line = line.strip() + for raw_line in f: + line = raw_line.strip() if line: try: entries.append(EventLogEntry.from_json(line)) @@ -604,9 +603,8 @@ def reconstruct_session_state( agent = entry.data.get("agent") model = entry.data.get("model") created_at = datetime.fromisoformat(entry.ts) - elif entry.event_type == "session.status": - if entry.data.get("name") == session_name: - status = AgenticSessionStatus(entry.data["status"]) + elif entry.event_type == "session.status" and entry.data.get("name") == session_name: + status = AgenticSessionStatus(entry.data["status"]) if session_id is None or environment_id is None or created_at is None: return None diff --git a/packages/doeff-agentic/src/doeff_agentic/handler.py b/packages/doeff-agentic/src/doeff_agentic/handler.py index 7285e16ff..68a583a26 100644 --- a/packages/doeff-agentic/src/doeff_agentic/handler.py +++ b/packages/doeff-agentic/src/doeff_agentic/handler.py @@ -1,6 +1,6 @@ """Backward-compatible re-export for legacy handler imports.""" -from .handlers.production import ( # noqa: F401 +from .handlers.production import ( AgenticHandler, WorkflowContext, agent_handler, diff --git a/packages/doeff-agentic/src/doeff_agentic/handlers/opencode.py b/packages/doeff-agentic/src/doeff_agentic/handlers/opencode.py index bdbe163c9..823934307 100644 --- a/packages/doeff-agentic/src/doeff_agentic/handlers/opencode.py +++ b/packages/doeff-agentic/src/doeff_agentic/handlers/opencode.py @@ -31,10 +31,10 @@ async def main(): from typing import Any from doeff import Await, Effect, Pass, Resume, do, slog + # REMOVED: make_doeff_generator no longer exists in doeff.do # from doeff.do import make_doeff_generator - -from ..effects import ( +from doeff_agentic.effects import ( AgenticAbortSession, AgenticCreateEnvironment, AgenticCreateSession, @@ -51,8 +51,8 @@ async def main(): AgenticSendMessage, AgenticSupportsCapability, ) -from ..event_log import EventLogWriter, WorkflowIndex -from ..exceptions import ( +from doeff_agentic.event_log import EventLogWriter, WorkflowIndex +from doeff_agentic.exceptions import ( AgenticDuplicateNameError, AgenticEnvironmentInUseError, AgenticEnvironmentNotFoundError, @@ -60,7 +60,7 @@ async def main(): AgenticSessionNotFoundError, AgenticTimeoutError, ) -from ..types import ( +from doeff_agentic.types import ( AgenticEndOfEvents, AgenticEnvironmentHandle, AgenticEnvironmentType, @@ -126,7 +126,6 @@ def patch(self, path: str, **kwargs: Any): def close(self): """Close client, returning Program[..., None].""" yield Await(self._client.aclose()) - return None # ============================================================================= @@ -252,7 +251,7 @@ def close(self) -> None: try: loop = asyncio.get_running_loop() - loop.create_task(self._client._client.aclose()) + self._close_task = loop.create_task(self._client._client.aclose()) except RuntimeError: # No running loop, create one for cleanup asyncio.run(self._client._client.aclose()) @@ -635,7 +634,6 @@ def _abort_session(): model=session.model, ) - return None return _abort_session() @@ -827,17 +825,16 @@ def handle_next_event(self, effect: AgenticNextEvent): continue # Map to our event types - if event_type == "session.updated": - if isinstance(props, dict): - status = props.get("status") - if status in ("done", "error", "aborted"): - self._close_sse(effect.session_id) - final_status = self._map_status(status) - result: AgenticEvent | AgenticEndOfEvents = AgenticEndOfEvents( - reason=f"session_{status}", - final_status=final_status, - ) - return result + if event_type == "session.updated" and isinstance(props, dict): + status = props.get("status") + if status in ("done", "error", "aborted"): + self._close_sse(effect.session_id) + final_status = self._map_status(status) + result: AgenticEvent | AgenticEndOfEvents = AgenticEndOfEvents( + reason=f"session_{status}", + final_status=final_status, + ) + return result result = AgenticEvent( event_type=event_type, @@ -991,7 +988,6 @@ def _refresh_session_status(self, session_id: str): self._update_session_status(session_id, status) except Exception: pass - return None def _map_status(self, status: str) -> AgenticSessionStatus: """Map OpenCode status to AgenticSessionStatus.""" @@ -1027,7 +1023,7 @@ def _wrapped(effect: Effect, k: Any): result = handler_fn(effect) if inspect.isgenerator(result): - resolved = yield make_doeff_generator(result) + resolved = yield make_doeff_generator(result) # noqa: F821 - legacy removed API reference is intentionally preserved return (yield Resume(k, resolved)) if _is_lazy_program_value(result): diff --git a/packages/doeff-agentic/src/doeff_agentic/handlers/production.py b/packages/doeff-agentic/src/doeff_agentic/handlers/production.py index 8af8a8ceb..fdbe4b7f1 100644 --- a/packages/doeff-agentic/src/doeff_agentic/handlers/production.py +++ b/packages/doeff-agentic/src/doeff_agentic/handlers/production.py @@ -11,13 +11,13 @@ """ +import contextlib import time from dataclasses import dataclass, field from datetime import datetime, timezone from pathlib import Path from typing import Any -from doeff import Effect, Pass, Resume, WithHandler, do from doeff_agents import ( AgentType, Capture, @@ -31,9 +31,17 @@ Stop, TmuxAgentHandler, ) -from doeff_agents.effects import CaptureEffect, LaunchEffect, MonitorEffect, SendEffect, SleepEffect, StopEffect +from doeff_agents.effects import ( + CaptureEffect, + LaunchEffect, + MonitorEffect, + SendEffect, + SleepEffect, + StopEffect, +) -from ..effects import ( +from doeff import Effect, Pass, Resume, WithHandler, do +from doeff_agentic.effects import ( AgentNotRunningError, CaptureOutputEffect, RunAgentEffect, @@ -43,8 +51,8 @@ WaitForStatusEffect, WaitForUserInputEffect, ) -from ..state import StateManager, generate_workflow_id -from ..types import AgentInfo, AgentStatus, WorkflowInfo, WorkflowStatus +from doeff_agentic.state import StateManager, generate_workflow_id +from doeff_agentic.types import AgentInfo, AgentStatus, WorkflowInfo, WorkflowStatus def _agent_type_from_str(s: str) -> AgentType: @@ -241,7 +249,6 @@ def send_message_program(self, effect: SendMessageEffect) -> None: effect.message, enter=effect.enter, ) - return None @do def wait_for_status_program(self, effect: WaitForStatusEffect) -> AgentStatus: @@ -304,7 +311,7 @@ def _resolve_session_handle(self, session_name: str) -> SessionHandle: return handle for name, candidate in self._context.sessions.items(): - if candidate.session_name == session_name or name == session_name: + if session_name in (candidate.session_name, name): return candidate raise AgentNotRunningError(session_name, "not_found") @@ -379,7 +386,7 @@ def handle_send_message(self, effect: SendMessageEffect) -> None: handle = self._context.sessions.get(effect.session_name) if handle is None: # Try full session name lookup - for name, h in self._context.sessions.items(): + for _name, h in self._context.sessions.items(): if h.session_name == effect.session_name: handle = h break @@ -528,14 +535,12 @@ def cleanup(self) -> None: """Clean up all agent sessions.""" for name in list(self._context.sessions.keys()): handle = self._context.sessions[name] - try: + with contextlib.suppress(Exception): self.tmux_handler.handle_stop(Stop(handle)) - except Exception: - pass del self._context.sessions[name] -def agentic_effectful_handlers( +def agentic_effectful_handlers( # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged workflow_id: str | None = None, workflow_name: str | None = None, state_dir: Path | str | None = None, @@ -557,7 +562,7 @@ def agentic_effectful_handlers( ) @do - def _handle(effect: Effect, k: Any): + def _handle(effect: Effect, k: Any): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, LaunchEffect): return (yield Resume(k, handler.tmux_handler.handle_launch(effect))) if isinstance(effect, MonitorEffect): diff --git a/packages/doeff-agentic/src/doeff_agentic/handlers/testing.py b/packages/doeff-agentic/src/doeff_agentic/handlers/testing.py index 2263313fb..cece0064b 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 + # REMOVED: make_doeff_generator no longer exists in doeff.do # from doeff.do import make_doeff_generator from doeff_agentic.effects import ( @@ -441,7 +442,7 @@ def _wrapped(effect: Effect, k: Any): result = handler_fn(effect) if inspect.isgenerator(result): - resolved = yield make_doeff_generator(result) + resolved = yield make_doeff_generator(result) # noqa: F821 - legacy removed API reference is intentionally preserved return (yield Resume(k, resolved)) if _is_lazy_program_value(result): diff --git a/packages/doeff-agentic/src/doeff_agentic/handlers/tmux.py b/packages/doeff-agentic/src/doeff_agentic/handlers/tmux.py index 55093fb28..b51c8a6ea 100644 --- a/packages/doeff-agentic/src/doeff_agentic/handlers/tmux.py +++ b/packages/doeff-agentic/src/doeff_agentic/handlers/tmux.py @@ -21,6 +21,7 @@ """ +import contextlib import hashlib import inspect import re @@ -34,10 +35,10 @@ from typing import Any from doeff import Effect, Pass, Resume, do + # REMOVED: make_doeff_generator no longer exists in doeff.do # from doeff.do import make_doeff_generator - -from ..effects import ( +from doeff_agentic.effects import ( AgenticAbortSession, AgenticCreateEnvironment, AgenticCreateSession, @@ -54,8 +55,8 @@ AgenticSendMessage, AgenticSupportsCapability, ) -from ..event_log import EventLogWriter, WorkflowIndex -from ..exceptions import ( +from doeff_agentic.event_log import EventLogWriter, WorkflowIndex +from doeff_agentic.exceptions import ( AgenticDuplicateNameError, AgenticEnvironmentInUseError, AgenticEnvironmentNotFoundError, @@ -65,7 +66,7 @@ AgenticTimeoutError, AgenticUnsupportedOperationError, ) -from ..types import ( +from doeff_agentic.types import ( AgenticEndOfEvents, AgenticEnvironmentHandle, AgenticEnvironmentType, @@ -298,10 +299,8 @@ def close(self) -> None: # Kill all sessions if self._workflow: for state in self._workflow.sessions.values(): - try: + with contextlib.suppress(Exception): self._backend.kill_session(f"doeff-{self._workflow.id}-{state.handle.name}") - except Exception: - pass # ------------------------------------------------------------------------- # Workflow Effects @@ -475,7 +474,7 @@ def handle_create_session( _session_config(session_name=session_name, work_dir=work_dir) ).pane_id except Exception as e: - raise AgenticServerError(f"Failed to create tmux session: {e}") + raise AgenticServerError(f"Failed to create tmux session: {e}") from e handle = AgenticSessionHandle( id=session_id, @@ -541,10 +540,8 @@ def handle_abort_session(self, effect: AgenticAbortSession) -> None: session_name = f"doeff-{self._workflow.id}-{name}" # Send Ctrl+C to abort - try: + with contextlib.suppress(Exception): send_keys(state.pane_id, "C-c", literal=False, enter=False) - except Exception: - pass # Kill the tmux session self._backend.kill_session(session_name) @@ -932,7 +929,7 @@ def _wrapped(effect: Effect, k: Any): result = handler_fn(effect) if inspect.isgenerator(result): - resolved = yield make_doeff_generator(result) + resolved = yield make_doeff_generator(result) # noqa: F821 - legacy removed API reference is intentionally preserved return (yield Resume(k, resolved)) if _is_lazy_program_value(result): diff --git a/packages/doeff-agentic/src/doeff_agentic/opencode_handler.py b/packages/doeff-agentic/src/doeff_agentic/opencode_handler.py index 19e2a4ff0..6f977b9e1 100644 --- a/packages/doeff-agentic/src/doeff_agentic/opencode_handler.py +++ b/packages/doeff-agentic/src/doeff_agentic/opencode_handler.py @@ -1,6 +1,6 @@ """Backward-compatible re-export for OpenCode handler imports.""" -from .handlers.opencode import OpenCodeHandler, opencode_handler # noqa: F401 +from .handlers.opencode import OpenCodeHandler, opencode_handler __all__ = [ "OpenCodeHandler", diff --git a/packages/doeff-agentic/src/doeff_agentic/state.py b/packages/doeff-agentic/src/doeff_agentic/state.py index b40f352b5..6b91cd514 100644 --- a/packages/doeff-agentic/src/doeff_agentic/state.py +++ b/packages/doeff-agentic/src/doeff_agentic/state.py @@ -19,6 +19,7 @@ """ +import contextlib import hashlib import json import os @@ -50,10 +51,8 @@ def _atomic_write(path: Path, content: str) -> None: os.rename(temp_path, path) except Exception: os.close(fd) - try: + with contextlib.suppress(OSError): os.unlink(temp_path) - except OSError: - pass raise @@ -427,8 +426,8 @@ def read_trace(self, workflow_id: str) -> list[dict[str, Any]]: entries: list[dict[str, Any]] = [] with trace_path.open() as f: - for line in f: - line = line.strip() + for raw_line in f: + line = raw_line.strip() if line: entries.append(json.loads(line)) return entries diff --git a/packages/doeff-agentic/src/doeff_agentic/tmux_handler.py b/packages/doeff-agentic/src/doeff_agentic/tmux_handler.py index a8fe5d85a..a49780e7f 100644 --- a/packages/doeff-agentic/src/doeff_agentic/tmux_handler.py +++ b/packages/doeff-agentic/src/doeff_agentic/tmux_handler.py @@ -1,6 +1,6 @@ """Backward-compatible re-export for tmux handler imports.""" -from .handlers.tmux import TmuxHandler, tmux_handler # noqa: F401 +from .handlers.tmux import TmuxHandler, tmux_handler __all__ = [ "TmuxHandler", diff --git a/packages/doeff-agentic/src/doeff_agentic/tui/app.py b/packages/doeff-agentic/src/doeff_agentic/tui/app.py index 675c07bc3..a38c11aab 100644 --- a/packages/doeff-agentic/src/doeff_agentic/tui/app.py +++ b/packages/doeff-agentic/src/doeff_agentic/tui/app.py @@ -5,7 +5,8 @@ from textual.app import App from textual.binding import Binding -from ..api import AgenticAPI +from doeff_agentic.api import AgenticAPI + from .screens import WorkflowListScreen @@ -19,7 +20,7 @@ class AgenticTUI(App[None]): } """ - BINDINGS = [ + BINDINGS = [ # noqa: RUF012 - framework class attribute is intentionally static metadata Binding("q", "quit", "Quit", show=True), ] diff --git a/packages/doeff-agentic/src/doeff_agentic/tui/screens.py b/packages/doeff-agentic/src/doeff_agentic/tui/screens.py index dd8348ac2..a9b99bd79 100644 --- a/packages/doeff-agentic/src/doeff_agentic/tui/screens.py +++ b/packages/doeff-agentic/src/doeff_agentic/tui/screens.py @@ -16,7 +16,8 @@ from textual.timer import Timer from textual.widgets import Footer, Header, Static -from ..types import WorkflowInfo +from doeff_agentic.types import WorkflowInfo + from .widgets import ( AgentListItem, CurrentActivityPane, @@ -35,7 +36,7 @@ class WorkflowListScreen(Screen[None]): and actions on selected workflow. """ - BINDINGS = [ + BINDINGS = [ # noqa: RUF012 - framework class attribute is intentionally static metadata Binding("enter", "watch", "Watch", show=True), Binding("s", "stop", "Stop", show=True), Binding("r", "refresh", "Refresh", show=True), @@ -229,7 +230,7 @@ class WorkflowWatchScreen(Screen[None]): └──────────────────────────────────────────────────────────────────┘ """ - BINDINGS = [ + BINDINGS = [ # noqa: RUF012 - framework class attribute is intentionally static metadata Binding("a", "attach", "Attach", show=True), Binding("l", "logs", "Logs", show=True), Binding("s", "stop", "Stop", show=True), @@ -375,7 +376,7 @@ def refresh_workflow(self) -> None: self.workflow_id, agent.name, lines=5 ) # Get last non-empty line as snippet - lines = [l for l in output.strip().split("\n") if l.strip()] + lines = [line for line in output.strip().split("\n") if line.strip()] agent_outputs[agent.name] = lines[-1][:50] if lines else "-" except Exception: agent_outputs[agent.name] = "-" @@ -515,7 +516,7 @@ def action_back(self) -> None: class AgentLogsScreen(Screen[None]): """Full-screen view of agent logs.""" - BINDINGS = [ + BINDINGS = [ # noqa: RUF012 - framework class attribute is intentionally static metadata Binding("q,escape", "back", "Back", show=True), Binding("r", "refresh", "Refresh", show=True), Binding("up,k", "scroll_up", "Up", show=False), diff --git a/packages/doeff-agentic/src/doeff_agentic/tui/widgets.py b/packages/doeff-agentic/src/doeff_agentic/tui/widgets.py index 36a17c250..04d9da4d9 100644 --- a/packages/doeff-agentic/src/doeff_agentic/tui/widgets.py +++ b/packages/doeff-agentic/src/doeff_agentic/tui/widgets.py @@ -14,7 +14,7 @@ from textual.app import ComposeResult from textual.widgets import Static -from ..types import AgentInfo, AgentStatus, WorkflowInfo, WorkflowStatus +from doeff_agentic.types import AgentInfo, AgentStatus, WorkflowInfo, WorkflowStatus def _format_relative_time(dt: datetime) -> str: @@ -296,7 +296,7 @@ def _render_content(self) -> None: self.update("\n".join(lines)) - def _get_status_description(self, wf: WorkflowInfo) -> str: + def _get_status_description(self, wf: WorkflowInfo) -> str: # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged """Get a human-readable status description.""" if wf.status == WorkflowStatus.BLOCKED: return "Waiting for input" diff --git a/packages/doeff-agentic/src/doeff_agentic/visual_interceptor.py b/packages/doeff-agentic/src/doeff_agentic/visual_interceptor.py index f393412c3..b07f55d6f 100644 --- a/packages/doeff-agentic/src/doeff_agentic/visual_interceptor.py +++ b/packages/doeff-agentic/src/doeff_agentic/visual_interceptor.py @@ -62,7 +62,7 @@ class VisualInterceptorConfig: def _get_timestamp() -> str: - return datetime.now().strftime("%H:%M:%S") + return datetime.now().strftime("%H:%M:%S") # noqa: DTZ005 - existing local wall-clock behavior is intentionally unchanged def _truncate(text: str, max_len: int) -> str: @@ -71,7 +71,7 @@ def _truncate(text: str, max_len: int) -> str: return text[: max_len - 3] + "..." -def _format_effect_details(effect: Any, config: VisualInterceptorConfig) -> str: +def _format_effect_details(effect: Any, config: VisualInterceptorConfig) -> str: # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged details: list[str] = [] effect_fields = getattr(effect, "__dataclass_fields__", {}) @@ -188,10 +188,7 @@ def transform(effect: Any) -> Any: result = yield effect elapsed = time.time() - start_time - if cfg.show_duration: - duration_str = f" [dim]({elapsed:.1f}s)[/dim]" - else: - duration_str = "" + duration_str = f" [dim]({elapsed:.1f}s)[/dim]" if cfg.show_duration else "" result_str = _format_result(result) console.print(f"{timestamp}[dim {color}]<-[/dim {color}] [dim]{result_str}{duration_str}[/dim]") diff --git a/packages/doeff-agentic/tests/test_effects.py b/packages/doeff-agentic/tests/test_effects.py index e670676aa..c0f4f0723 100644 --- a/packages/doeff-agentic/tests/test_effects.py +++ b/packages/doeff-agentic/tests/test_effects.py @@ -54,7 +54,7 @@ def test_effect_is_frozen(self): effect = RunAgent(config) with pytest.raises(AttributeError): - setattr(effect, "poll_interval", 5.0) + effect.poll_interval = 5.0 class TestSendMessageEffect: diff --git a/packages/doeff-agentic/tests/test_visual_interceptor.py b/packages/doeff-agentic/tests/test_visual_interceptor.py index 0ec85511d..2a5afbd3a 100644 --- a/packages/doeff-agentic/tests/test_visual_interceptor.py +++ b/packages/doeff-agentic/tests/test_visual_interceptor.py @@ -3,8 +3,6 @@ from io import StringIO import doeff_vm - -from doeff import Effect, WithHandler, default_handlers, do, run, slog from doeff_agentic.effects import AgenticSupportsCapability from doeff_agentic.visual_interceptor import ( VisualInterceptorConfig, @@ -13,6 +11,8 @@ ) from rich.console import Console +from doeff import Effect, WithHandler, default_handlers, do, run, slog + @do def _capability_handler(effect: Effect, k): From 1363ca5066aba3cd4c20206f7ba189b89670b433 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:57 +0900 Subject: [PATCH 03/35] Clean Ruff violations in doeff-agents --- .../doeff-agents/examples/01_basic_session.py | 52 ++++----- .../examples/02_context_manager.py | 70 ++++++------ .../examples/03_async_monitoring.py | 106 ++++++++--------- .../examples/04_custom_adapter.py | 35 +++--- .../examples/05_status_callbacks.py | 69 ++++++----- .../doeff-agents/examples/06_effects_api.py | 107 +++++++++--------- .../src/doeff_agents/adapters/__init__.py | 4 +- .../src/doeff_agents/handlers/__init__.py | 2 +- .../src/doeff_agents/handlers/production.py | 2 +- .../src/doeff_agents/mcp_server.py | 9 +- .../src/doeff_agents/result_validation.py | 4 +- .../tests/test_l2_session_algebra.py | 5 +- .../doeff-agents/tests/test_mcp_server.py | 14 +-- .../tests/test_mcp_server_loop.py | 17 +-- .../tests/test_onboarding_dismiss.py | 6 +- .../tests/test_refactored_effects.py | 5 +- 16 files changed, 246 insertions(+), 261 deletions(-) diff --git a/packages/doeff-agents/examples/01_basic_session.py b/packages/doeff-agents/examples/01_basic_session.py index 0539c904c..f170b9f15 100644 --- a/packages/doeff-agents/examples/01_basic_session.py +++ b/packages/doeff-agents/examples/01_basic_session.py @@ -19,19 +19,15 @@ import time from pathlib import Path -from doeff import do -from doeff.effects.writer import slog -from doeff_time import Delay -from doeff_preset import preset_handlers - from _runtime import run_program +from doeff.effects.writer import slog from doeff_agents import ( AgentType, Capture, Launch, LaunchConfig, - Monitor, MockSessionScript, + Monitor, SessionHandle, SessionStatus, Stop, @@ -39,61 +35,65 @@ configure_mock_session, mock_agent_handlers, ) +from doeff_preset import preset_handlers +from doeff_time import Delay + +from doeff import do @do def basic_session_workflow(session_name: str, config: LaunchConfig): """Run a basic agent session using effects. - + This program yields fine-grained effects that are interpreted by the runtime's handlers. """ yield slog(step="launch", session_name=session_name, work_dir=str(config.work_dir)) yield slog(step="launch", agent_type=config.agent_type.value) - + # Launch the session handle: SessionHandle = yield Launch(session_name, config) yield slog(step="launched", pane_id=handle.pane_id) - + try: # Monitor the session until terminal state or timeout max_iterations = 60 iteration = 0 final_status = SessionStatus.PENDING - + while iteration < max_iterations: observation = yield Monitor(handle) final_status = observation.status - + if observation.output_changed: yield slog( step="status", iteration=iteration, status=observation.status.value, ) - + if observation.is_terminal: yield slog(step="terminal", status=observation.status.value) break - + # If blocked (waiting for input), log it if observation.status == SessionStatus.BLOCKED: yield slog(step="blocked", msg="Agent is waiting for input") - + iteration += 1 yield Delay(1.0) - + # Capture final output output: str = yield Capture(handle, lines=30) yield slog(step="output", lines=30, preview=output[-200:] if output else "") - + return { "session_name": session_name, "final_status": final_status.value, "output": output, "iterations": iteration, } - + finally: # Always clean up the session yield slog(step="stopping", session_name=session_name) @@ -106,7 +106,7 @@ async def run_with_mock_handlers() -> None: print("=" * 60) print("Running with mock handlers") print("=" * 60) - + session_name = f"demo-{int(time.time())}" # Configure mock session behavior @@ -119,7 +119,7 @@ async def run_with_mock_handlers() -> None: (SessionStatus.DONE, "Task completed!\n\nFiles:\n- README.md\n- main.py"), ]), ) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), @@ -137,27 +137,27 @@ async def run_with_mock_handlers() -> None: async def run_with_real_tmux() -> None: """Run the example with real tmux (requires tmux + claude CLI).""" import shutil - + if not shutil.which("tmux"): print("tmux not available, skipping real tmux example") return - + if not shutil.which("claude"): print("Claude CLI not available, skipping real tmux example") return - + print("\n" + "=" * 60) print("Running with real tmux") print("=" * 60) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), prompt="List the files in the current directory and describe what you see.", ) - + session_name = f"demo-{int(time.time())}" - + result = await run_program( basic_session_workflow(session_name, config), scoped_handlers=(preset_handlers(),), @@ -169,7 +169,7 @@ async def run_with_real_tmux() -> None: async def main() -> None: """Run the examples.""" await run_with_mock_handlers() - + # Uncomment to run with real tmux (requires tmux + claude CLI) # await run_with_real_tmux() diff --git a/packages/doeff-agents/examples/02_context_manager.py b/packages/doeff-agents/examples/02_context_manager.py index b43f30710..1d3ea5b36 100644 --- a/packages/doeff-agents/examples/02_context_manager.py +++ b/packages/doeff-agents/examples/02_context_manager.py @@ -17,12 +17,8 @@ import time from pathlib import Path -from doeff import do -from doeff.effects.writer import slog -from doeff_time import Delay -from doeff_preset import preset_handlers - from _runtime import run_program +from doeff.effects.writer import slog from doeff_agents import ( AgentType, Capture, @@ -37,38 +33,42 @@ mock_agent_handlers, with_session, ) +from doeff_preset import preset_handlers +from doeff_time import Delay + +from doeff import do @do def interactive_workflow(handle: SessionHandle): """Custom logic to run within a session. - + This function yields effects and is called within with_session bracket. """ yield slog(step="interactive", msg="Starting interactive workflow") - + max_interactions = 3 interaction_count = 0 iteration = 0 - + while iteration < 120: # 2 minute timeout observation = yield Monitor(handle) - + if observation.output_changed: yield slog( step="status", iteration=iteration, status=observation.status.value, ) - + if observation.is_terminal: yield slog(step="terminal", status=observation.status.value) break - + # Interact with the agent when blocked if observation.status == SessionStatus.BLOCKED and interaction_count < max_interactions: interaction_count += 1 - + # Capture current output to see what the agent did output = yield Capture(handle, lines=20) yield slog( @@ -76,7 +76,7 @@ def interactive_workflow(handle: SessionHandle): interaction=interaction_count, preview=output[-300:] if output else "", ) - + # Send follow-up instruction based on interaction count if interaction_count == 1: yield slog(step="sending", msg="Add docstring and type hints") @@ -87,14 +87,14 @@ def interactive_workflow(handle: SessionHandle): elif interaction_count == 3: yield slog(step="sending", msg="Done, thank you") yield Send(handle, "That's perfect, thank you!") - + iteration += 1 yield Delay(1.0) - + # Final output final_output = yield Capture(handle, lines=50) yield slog(step="final", output_length=len(final_output)) - + return { "interactions": interaction_count, "iterations": iteration, @@ -105,18 +105,18 @@ def interactive_workflow(handle: SessionHandle): @do def run_with_bracket(session_name: str, config: LaunchConfig): """Run a session using the with_session bracket pattern. - + with_session ensures Stop is called even if an exception occurs. """ yield slog(step="start", session_name=session_name) - + # with_session is a bracket: Launch -> use -> Stop (always) result = yield do(with_session)( session_name, config, interactive_workflow.original_generator, ) - + yield slog(step="complete", interactions=result["interactions"]) return result @@ -124,33 +124,33 @@ def run_with_bracket(session_name: str, config: LaunchConfig): @do def demonstrate_exception_safety(session_name: str, config: LaunchConfig): """Show that cleanup happens even when exceptions occur. - + The with_session bracket ensures Stop is called in the finally block. """ yield slog(step="exception_demo", msg="Starting exception safety demo") - + def raise_after_work(handle: SessionHandle): """Inner function that simulates work then raises.""" yield slog(step="work", msg="Doing some work...") - + # Monitor once observation = yield Monitor(handle) yield slog(step="monitored", status=observation.status.value) - + # Simulate some work yield Delay(1.0) - + # Simulate an error yield slog(step="error", msg="About to raise exception!") raise RuntimeError("Simulated error during processing!") - + try: _ = yield do(with_session)(session_name, config, raise_after_work) except RuntimeError as e: yield slog(step="caught", error=str(e)) yield slog(step="cleanup", msg="Session was still cleaned up automatically!") return {"error": str(e), "cleanup_successful": True} - + return {"error": None, "cleanup_successful": True} @@ -159,7 +159,7 @@ async def run_interactive_example() -> None: print("=" * 60) print("Interactive Workflow with Bracket Pattern") print("=" * 60) - + session_name = f"factorial-demo-{int(time.time())}" # Configure mock behavior @@ -175,7 +175,7 @@ async def run_interactive_example() -> None: (SessionStatus.DONE, "Task completed!\n\ndef factorial(n: int) -> int:\n ..."), ]), ) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), @@ -205,7 +205,7 @@ async def run_exception_demo() -> None: (SessionStatus.RUNNING, "Still working..."), ]), ) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), @@ -223,23 +223,23 @@ async def run_exception_demo() -> None: async def run_with_real_tmux() -> None: """Run with real tmux (requires tmux + claude CLI).""" import shutil - + if not shutil.which("tmux") or not shutil.which("claude"): print("tmux or Claude CLI not available, skipping real tmux example") return - + print("\n" + "=" * 60) print("Running with real tmux") print("=" * 60) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), prompt="Create a simple Python function that calculates the factorial of a number.", ) - + session_name = f"factorial-{int(time.time())}" - + result = await run_program( run_with_bracket(session_name, config), scoped_handlers=(preset_handlers(),), @@ -252,7 +252,7 @@ async def main() -> None: """Run all examples.""" await run_interactive_example() await run_exception_demo() - + # Uncomment to run with real tmux # await run_with_real_tmux() diff --git a/packages/doeff-agents/examples/03_async_monitoring.py b/packages/doeff-agents/examples/03_async_monitoring.py index 8b973174c..25a80deb8 100644 --- a/packages/doeff-agents/examples/03_async_monitoring.py +++ b/packages/doeff-agents/examples/03_async_monitoring.py @@ -16,73 +16,73 @@ import time from pathlib import Path -from doeff import do -from doeff.effects.writer import slog -from doeff_time import Delay -from doeff_preset import preset_handlers - from _runtime import run_program +from doeff.effects.writer import slog from doeff_agents import ( AgentType, Capture, - MockSessionScript, Launch, LaunchConfig, + MockSessionScript, Monitor, Observation, SessionHandle, SessionStatus, Stop, agent_effectful_handlers, - mock_agent_handlers, configure_mock_session, + mock_agent_handlers, ) +from doeff_preset import preset_handlers +from doeff_time import Delay + +from doeff import do @do def single_session_workflow(session_name: str, config: LaunchConfig): """Run a single session using effects. - + Yields effects interpreted by doeff_vm handlers. """ yield slog(step="start", session_name=session_name) - + handle: SessionHandle = yield Launch(session_name, config) yield slog(step="launched", pane_id=handle.pane_id) - + final_status = SessionStatus.PENDING iteration = 0 - + try: while iteration < 120: observation: Observation = yield Monitor(handle) final_status = observation.status - + if observation.output_changed: yield slog( step="status_change", status=observation.status.value, ) - + if observation.pr_url: yield slog(step="pr_detected", url=observation.pr_url) - + if observation.is_terminal: break - + iteration += 1 yield Delay(0.5) - + output = yield Capture(handle, lines=20) yield slog(step="complete", status=final_status.value) - + return { "session_name": session_name, "status": final_status.value, "output": output, "iterations": iteration, } - + finally: yield Stop(handle) @@ -90,33 +90,33 @@ def single_session_workflow(session_name: str, config: LaunchConfig): @do def parallel_tasks_workflow(tasks: list[tuple[str, str]]): """Run multiple agent tasks, demonstrating parallel-like patterns. - + Note: True parallelism requires launching separate runtimes or using asyncio.gather at the Python level. This example shows how to structure the effects for sequential execution. - + For true parallel execution, see run_truly_parallel() below. """ yield slog(step="parallel_start", task_count=len(tasks)) - + results = [] - + for prompt, name_suffix in tasks: config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), prompt=prompt, ) - + session_name = f"parallel-{name_suffix}-{int(time.time())}" yield slog(step="task_start", name=name_suffix) - + # Run each task (sequentially in this case) result = yield from single_session_workflow(session_name, config) results.append((name_suffix, result)) - + yield slog(step="task_complete", name=name_suffix, status=result["status"]) - + yield slog(step="parallel_complete", completed=len(results)) return results @@ -124,12 +124,12 @@ def parallel_tasks_workflow(tasks: list[tuple[str, str]]): @do def interleaved_monitoring_workflow(configs: list[tuple[str, LaunchConfig]]): """Monitor multiple sessions with interleaved checks. - + This pattern launches all sessions first, then monitors them in a round-robin fashion until all are terminal. """ yield slog(step="interleaved_start", session_count=len(configs)) - + # Launch all sessions handles: list[tuple[str, SessionHandle]] = [] for suffix, config in configs: @@ -137,25 +137,25 @@ def interleaved_monitoring_workflow(configs: list[tuple[str, LaunchConfig]]): handle = yield Launch(session_name, config) handles.append((suffix, handle)) yield slog(step="launched", name=suffix, pane_id=handle.pane_id) - + # Track status for each session statuses = {suffix: SessionStatus.PENDING for suffix, _ in handles} - + try: timeout = 120 start_time = time.time() - - while not all(s in (SessionStatus.DONE, SessionStatus.FAILED, SessionStatus.EXITED) + + while not all(s in (SessionStatus.DONE, SessionStatus.FAILED, SessionStatus.EXITED) for s in statuses.values()): if time.time() - start_time > timeout: yield slog(step="timeout", msg="Timeout reached") break - + # Check each session for suffix, handle in handles: if statuses[suffix] not in (SessionStatus.DONE, SessionStatus.FAILED, SessionStatus.EXITED): observation = yield Monitor(handle) - + if observation.status != statuses[suffix]: statuses[suffix] = observation.status yield slog( @@ -163,9 +163,9 @@ def interleaved_monitoring_workflow(configs: list[tuple[str, LaunchConfig]]): name=suffix, status=observation.status.value, ) - + yield Delay(0.5) - + # Capture final outputs results = [] for suffix, handle in handles: @@ -175,10 +175,10 @@ def interleaved_monitoring_workflow(configs: list[tuple[str, LaunchConfig]]): "status": statuses[suffix].value, "output": output, }) - + yield slog(step="interleaved_complete", results_count=len(results)) return results - + finally: # Stop all sessions for suffix, handle in handles: @@ -199,7 +199,7 @@ async def run_with_mock_runtime() -> None: (SessionStatus.DONE, "Code flows like water\nBugs emerge then disappear\nTests pass, peace returns"), ]), ) - + # For mock, we need to pre-configure the session name session_name = f"async-demo-{int(time.time())}" configure_mock_session( @@ -209,7 +209,7 @@ async def run_with_mock_runtime() -> None: (SessionStatus.DONE, "Code flows like water\nBugs emerge then disappear\nTests pass, peace returns"), ]), ) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), @@ -226,24 +226,24 @@ async def run_with_mock_runtime() -> None: async def run_truly_parallel() -> None: """Run multiple sessions truly in parallel using asyncio.gather. - + This demonstrates how to achieve true parallelism by running multiple async_run invocations concurrently. """ print("\n" + "=" * 60) print("Running truly parallel sessions") print("=" * 60) - + tasks_config = [ ("Write a function to reverse a string", "reverse-string"), ("Write a function to check if a number is prime", "is-prime"), ("Write a function to calculate fibonacci numbers", "fibonacci"), ] - + async def run_single_task(prompt: str, suffix: str) -> dict: """Run a single task with its own runtime.""" session_name = f"parallel-{suffix}-{int(time.time())}" - + # Each task gets its own mock configuration configure_mock_session( session_name, @@ -252,7 +252,7 @@ async def run_single_task(prompt: str, suffix: str) -> dict: (SessionStatus.DONE, f"Completed {suffix}!"), ]), ) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), @@ -267,7 +267,7 @@ async def run_single_task(prompt: str, suffix: str) -> dict: if hasattr(type(run_result), "value"): return run_result.value return run_result - + # Run all tasks truly in parallel start_time = time.time() results = await asyncio.gather(*[ @@ -275,7 +275,7 @@ async def run_single_task(prompt: str, suffix: str) -> dict: for prompt, suffix in tasks_config ]) elapsed = time.time() - start_time - + print(f"\nAll sessions completed in {elapsed:.1f}s") for result in results: print(f" {result['session_name']}: {result['status']}") @@ -284,23 +284,23 @@ async def run_single_task(prompt: str, suffix: str) -> dict: async def run_with_real_tmux() -> None: """Run with real tmux (requires tmux + claude CLI).""" import shutil - + if not shutil.which("tmux") or not shutil.which("claude"): print("tmux or Claude CLI not available, skipping") return - + print("\n" + "=" * 60) print("Running with real tmux") print("=" * 60) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), prompt="Write a haiku about programming.", ) - + session_name = f"async-real-{int(time.time())}" - + result = await run_program( single_session_workflow(session_name, config), scoped_handlers=(preset_handlers(),), @@ -313,7 +313,7 @@ async def main() -> None: """Run all async examples.""" await run_with_mock_runtime() await run_truly_parallel() - + # Uncomment to run with real tmux # await run_with_real_tmux() diff --git a/packages/doeff-agents/examples/04_custom_adapter.py b/packages/doeff-agents/examples/04_custom_adapter.py index 351574796..5e75e45d0 100644 --- a/packages/doeff-agents/examples/04_custom_adapter.py +++ b/packages/doeff-agents/examples/04_custom_adapter.py @@ -17,12 +17,8 @@ import time from pathlib import Path -from doeff import do -from doeff.effects.writer import slog -from doeff_time import Delay -from doeff_preset import preset_handlers - from _runtime import run_program +from doeff.effects.writer import slog from doeff_agents import ( AgentType, Capture, @@ -38,7 +34,10 @@ mock_agent_handlers, ) from doeff_agents.adapters.base import AgentAdapter, InjectionMethod +from doeff_preset import preset_handlers +from doeff_time import Delay +from doeff import do # ============================================================================= # Example 1: Simple Custom Adapter (ARG injection) @@ -150,13 +149,13 @@ class ContinueDevAdapter(AgentAdapter): """ # Custom UI patterns for this specific agent - UI_PATTERNS: list[str] = [ + UI_PATTERNS: list[str] = [ # noqa: RUF012 - framework class attribute is intentionally static metadata "Continue>", "Thinking...", "[continue]", ] - BLOCKED_PATTERNS: list[str] = [ + BLOCKED_PATTERNS: list[str] = [ # noqa: RUF012 - framework class attribute is intentionally static metadata "Press Enter to confirm", "Choose an option:", "Continue>", @@ -198,38 +197,38 @@ def status_bar_lines(self) -> int: @do def custom_adapter_workflow(session_name: str, config: LaunchConfig): """Run a session with any adapter using effects. - + The adapter is determined by the agent_type in the config. """ yield slog(step="start", session_name=session_name, agent_type=config.agent_type.value) - + handle: SessionHandle = yield Launch(session_name, config) yield slog(step="launched", pane_id=handle.pane_id) - + final_status = SessionStatus.PENDING - + try: - for iteration in range(30): + for _iteration in range(30): observation = yield Monitor(handle) final_status = observation.status - + if observation.output_changed: yield slog(step="status", status=observation.status.value) - + if observation.is_terminal: break - + yield Delay(1.0) - + output = yield Capture(handle, lines=30) yield slog(step="complete", status=final_status.value) - + return { "session_name": session_name, "status": final_status.value, "output": output, } - + finally: yield Stop(handle) diff --git a/packages/doeff-agents/examples/05_status_callbacks.py b/packages/doeff-agents/examples/05_status_callbacks.py index ee3841721..2280116d8 100644 --- a/packages/doeff-agents/examples/05_status_callbacks.py +++ b/packages/doeff-agents/examples/05_status_callbacks.py @@ -7,7 +7,7 @@ With the effects-based approach: - Status changes are logged via slog effects -- PR URL detection is logged via slog effects +- PR URL detection is logged via slog effects - Event collection is done through the runtime's log accumulation - All events are structured and queryable @@ -24,12 +24,8 @@ from datetime import datetime, timezone from pathlib import Path -from doeff import do -from doeff.effects.writer import slog -from doeff_time import Delay -from doeff_preset import preset_handlers - from _runtime import run_program +from doeff.effects.writer import slog from doeff_agents import ( AgentType, Capture, @@ -45,7 +41,10 @@ configure_mock_session, mock_agent_handlers, ) +from doeff_preset import preset_handlers +from doeff_time import Delay +from doeff import do # ============================================================================= # Event Types for Structured Logging @@ -72,25 +71,25 @@ class SessionEvent: @do def monitored_session_workflow(session_name: str, config: LaunchConfig): """Run a session with comprehensive slog-based monitoring. - + Instead of callbacks, we yield slog effects for every event. The runtime accumulates these logs for later analysis. """ yield slog(step="session_start", session_name=session_name, timestamp=datetime.now(timezone.utc).isoformat()) - + handle: SessionHandle = yield Launch(session_name, config) yield slog(step="launched", pane_id=handle.pane_id) - + previous_status = SessionStatus.PENDING final_status = SessionStatus.PENDING pr_url: str | None = None iteration = 0 - + try: for iteration in range(60): observation: Observation = yield Monitor(handle) final_status = observation.status - + # Log status changes (equivalent to on_status_change callback) if observation.status != previous_status: yield slog( @@ -100,7 +99,7 @@ def monitored_session_workflow(session_name: str, config: LaunchConfig): new_status=observation.status.value, timestamp=datetime.now(timezone.utc).isoformat(), ) - + # Log specific status events if observation.status == SessionStatus.BLOCKED: yield slog( @@ -130,9 +129,9 @@ def monitored_session_workflow(session_name: str, config: LaunchConfig): event_type="completed", msg=f"Session completed ({iteration}s)", ) - + previous_status = observation.status - + # Log PR detection (equivalent to on_pr_detected callback) if observation.pr_url and not pr_url: pr_url = observation.pr_url @@ -142,12 +141,12 @@ def monitored_session_workflow(session_name: str, config: LaunchConfig): pr_url=pr_url, timestamp=datetime.now(timezone.utc).isoformat(), ) - + if observation.is_terminal: break - + yield Delay(1.0) - + # Final output capture output = yield Capture(handle, lines=50) yield slog( @@ -157,7 +156,7 @@ def monitored_session_workflow(session_name: str, config: LaunchConfig): pr_url=pr_url, duration_iterations=iteration, ) - + return { "session_name": session_name, "final_status": final_status.value, @@ -165,7 +164,7 @@ def monitored_session_workflow(session_name: str, config: LaunchConfig): "output": output, "iterations": iteration, } - + finally: yield Stop(handle) @@ -173,7 +172,7 @@ def monitored_session_workflow(session_name: str, config: LaunchConfig): @do def event_collecting_workflow(session_name: str, config: LaunchConfig): """Workflow that collects events for post-run analysis. - + Uses slog with structured data that can be extracted from the runtime's accumulated log. """ @@ -182,45 +181,45 @@ def event_collecting_workflow(session_name: str, config: LaunchConfig): event_type="collection_started", session_name=session_name, ) - + result = yield from monitored_session_workflow(session_name, config) - + yield slog( step="collector_end", event_type="collection_complete", total_events="see_accumulated_log", final_status=result["final_status"], ) - + return result @do def notification_workflow(session_name: str, config: LaunchConfig): """Workflow demonstrating notification-style events. - + In production, these slogs could be processed by a handler that sends actual notifications (Slack, email, etc.). """ start_time = time.time() - + yield slog( step="notifier_start", event_type="notification", channel="monitoring", msg=f"Starting session: {session_name}", ) - + handle: SessionHandle = yield Launch(session_name, config) previous_status = SessionStatus.PENDING final_status = SessionStatus.PENDING - + try: - for iteration in range(60): + for _iteration in range(60): observation = yield Monitor(handle) final_status = observation.status elapsed = time.time() - start_time - + if observation.status != previous_status: # Notification-style logging if observation.status == SessionStatus.BLOCKED: @@ -253,9 +252,9 @@ def notification_workflow(session_name: str, config: LaunchConfig): severity="info", msg=f"[{session_name}] Session completed ({elapsed:.0f}s)", ) - + previous_status = observation.status - + if observation.pr_url: yield slog( step="notification", @@ -263,19 +262,19 @@ def notification_workflow(session_name: str, config: LaunchConfig): severity="info", msg=f"[{session_name}] PR Created: {observation.pr_url}", ) - + if observation.is_terminal: break - + yield Delay(1.0) - + output = yield Capture(handle, lines=30) return { "session_name": session_name, "final_status": final_status.value, "output": output, } - + finally: yield Stop(handle) diff --git a/packages/doeff-agents/examples/06_effects_api.py b/packages/doeff-agents/examples/06_effects_api.py index 48bb4579b..aa664f737 100644 --- a/packages/doeff-agents/examples/06_effects_api.py +++ b/packages/doeff-agents/examples/06_effects_api.py @@ -23,12 +23,8 @@ import time from pathlib import Path -from doeff import do -from doeff.effects.writer import slog -from doeff_time import Delay -from doeff_preset import preset_handlers - from _runtime import run_program +from doeff.effects.writer import slog from doeff_agents import ( # Types AgentType, @@ -51,7 +47,10 @@ run_agent_to_completion, with_session, ) +from doeff_preset import preset_handlers +from doeff_time import Delay +from doeff import do # ============================================================================= # Example 1: Direct Effect Usage with @do @@ -61,39 +60,39 @@ @do def direct_effects_workflow(session_name: str, config: LaunchConfig): """Demonstrate using effects directly with @do decorator. - + Each yield is an effect that gets interpreted by the runtime's handlers. """ yield slog(step="example1", msg="Direct Effect Usage") - + # Launch effect handle: SessionHandle = yield Launch(session_name, config) yield slog(step="launched", session_name=session_name, pane_id=handle.pane_id) - + final_status = SessionStatus.PENDING - + # Monitor until terminal try: for iteration in range(10): observation: Observation = yield Monitor(handle) final_status = observation.status yield slog(step="monitor", iteration=iteration, status=observation.status.value) - + if observation.is_terminal: break - + # Delay between polls yield Delay(1.0) - + # Capture final output output: str = yield Capture(handle, lines=50) yield slog(step="captured", output_length=len(output)) - + return { "status": final_status.value, "output": output, } - + finally: # Stop session yield Stop(handle) @@ -108,12 +107,12 @@ def direct_effects_workflow(session_name: str, config: LaunchConfig): @do def high_level_programs_workflow(session_name: str, config: LaunchConfig): """Demonstrate using high-level Programs with @do. - + Programs like run_agent_to_completion compose fine-grained effects into reusable workflows. """ yield slog(step="example2", msg="High-Level Programs") - + # run_agent_to_completion is a generator that yields effects # We use yield from to delegate to it result = yield from run_agent_to_completion( @@ -121,14 +120,14 @@ def high_level_programs_workflow(session_name: str, config: LaunchConfig): config, poll_interval=0.5, ) - + yield slog( step="complete", succeeded=result.succeeded, status=result.final_status.value, iterations=result.iterations, ) - + return { "succeeded": result.succeeded, "status": result.final_status.value, @@ -145,35 +144,35 @@ def high_level_programs_workflow(session_name: str, config: LaunchConfig): @do def bracket_pattern_workflow(session_name: str, config: LaunchConfig): """Demonstrate the with_session bracket pattern. - + with_session ensures Stop is called even on exceptions. """ yield slog(step="example3", msg="Bracket Pattern (with_session)") - + def use_session(handle: SessionHandle): """Custom logic to run within the session.""" # Monitor once obs = yield Monitor(handle) yield slog(step="initial_status", status=obs.status.value) - + # Wait for BLOCKED status while obs.status != SessionStatus.BLOCKED: yield Delay(0.5) obs = yield Monitor(handle) - + # Send a follow-up message yield Send(handle, "Thanks, that's helpful!") yield slog(step="sent_followup") - + # Capture output output = yield Capture(handle, lines=20) return output - + # with_session ensures Stop is called even on error output = yield from with_session(session_name, config, use_session) - + yield slog(step="bracket_complete", output_length=len(output) if output else 0) - + return {"output": output} @@ -185,37 +184,37 @@ def use_session(handle: SessionHandle): @do def testable_workflow(session_name: str, config: LaunchConfig): """A workflow designed for easy testing. - + mock_agent_handlers allows deterministic testing without real tmux. """ yield slog(step="example4", msg="Testing with mock handlers") - + handle = yield Launch(session_name, config) - + observations = [] final_status = SessionStatus.PENDING - + try: for _ in range(10): obs = yield Monitor(handle) observations.append(obs.status) final_status = obs.status - + yield slog(step="observed", status=obs.status.value) - + if obs.is_terminal: break - + yield Delay(0.5) - + output = yield Capture(handle, lines=50) - + return { "observations": [s.value for s in observations], "final_status": final_status.value, "output": output, } - + finally: yield Stop(handle) @@ -230,7 +229,7 @@ async def run_direct_effects_example() -> None: print("=" * 60) print("Example 1: Direct Effect Usage") print("=" * 60) - + session_name = f"demo-{int(time.time())}" configure_mock_session( @@ -241,7 +240,7 @@ async def run_direct_effects_example() -> None: (SessionStatus.DONE, "Task completed!"), ]), ) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), @@ -261,7 +260,7 @@ async def run_high_level_programs_example() -> None: print("\n" + "=" * 60) print("Example 2: High-Level Programs") print("=" * 60) - + session_name = f"program-demo-{int(time.time())}" configure_mock_session( @@ -271,7 +270,7 @@ async def run_high_level_programs_example() -> None: (SessionStatus.DONE, "Complete!"), ]), ) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), @@ -291,7 +290,7 @@ async def run_bracket_pattern_example() -> None: print("\n" + "=" * 60) print("Example 3: Bracket Pattern (with_session)") print("=" * 60) - + session_name = f"bracket-demo-{int(time.time())}" configure_mock_session( @@ -301,7 +300,7 @@ async def run_bracket_pattern_example() -> None: (SessionStatus.BLOCKED, "Need input..."), ]), ) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), @@ -321,7 +320,7 @@ async def run_testing_example() -> None: print("\n" + "=" * 60) print("Example 4: Testing with Mock Handlers") print("=" * 60) - + session_name = f"test-{int(time.time())}" configure_mock_session( @@ -333,7 +332,7 @@ async def run_testing_example() -> None: (SessionStatus.DONE, "Changes applied!"), ]), ) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), @@ -346,10 +345,10 @@ async def run_testing_example() -> None: custom_handlers=mock_agent_handlers(), ) result_value = result.value if hasattr(type(result), "value") else result - + # Verify behavior print("\nTest assertions:") - expected_statuses = ['running', 'running', 'blocked', 'done'] + expected_statuses = ["running", "running", "blocked", "done"] if isinstance(result_value, dict): print(f" Final status is DONE: {result_value['final_status'] == 'done'}") print(f" Observed expected statuses: {result_value['observations'] == expected_statuses}") @@ -362,33 +361,33 @@ async def run_testing_example() -> None: async def run_with_real_tmux() -> None: """Run with real tmux (requires tmux + claude CLI).""" import shutil - + if not shutil.which("tmux"): print("tmux not available, skipping") return - + if not shutil.which("claude"): print("Claude CLI not available, skipping") return - + print("\n" + "=" * 60) print("Example 5: Real Tmux Handler") print("=" * 60) - + config = LaunchConfig( agent_type=AgentType.CLAUDE, work_dir=Path.cwd(), prompt="Write a hello world function", ) - + session_name = f"real-tmux-{int(time.time())}" - + result = await run_program( direct_effects_workflow(session_name, config), scoped_handlers=(preset_handlers(),), custom_handlers=agent_effectful_handlers(), ) - + print(f"\nResult: {result}") @@ -398,7 +397,7 @@ async def main() -> None: await run_high_level_programs_example() await run_bracket_pattern_example() await run_testing_example() - + # Real tmux (uncomment to test) # await run_with_real_tmux() diff --git a/packages/doeff-agents/src/doeff_agents/adapters/__init__.py b/packages/doeff-agents/src/doeff_agents/adapters/__init__.py index 6c0d3aac9..2464ae76b 100644 --- a/packages/doeff-agents/src/doeff_agents/adapters/__init__.py +++ b/packages/doeff-agents/src/doeff_agents/adapters/__init__.py @@ -1,6 +1,7 @@ """Agent adapters for different coding agents.""" -from .base import AgentAdapter, AgentType, InjectionMethod, LaunchConfig, LaunchParams +from .base import AgentAdapter, AgentType, InjectionMethod, LaunchParams +from .base import LaunchConfig as LaunchConfig from .claude import ClaudeAdapter from .codex import CodexAdapter from .gemini import GeminiAdapter @@ -12,5 +13,6 @@ "CodexAdapter", "GeminiAdapter", "InjectionMethod", + "LaunchConfig", "LaunchParams", ] diff --git a/packages/doeff-agents/src/doeff_agents/handlers/__init__.py b/packages/doeff-agents/src/doeff_agents/handlers/__init__.py index 94587e83a..1c06e547e 100644 --- a/packages/doeff-agents/src/doeff_agents/handlers/__init__.py +++ b/packages/doeff-agents/src/doeff_agents/handlers/__init__.py @@ -71,7 +71,7 @@ ) -def dispatch_effect(handler: AgentHandler, effect: Any) -> Any: +def dispatch_effect(handler: AgentHandler, effect: Any) -> Any: # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged """Dispatch an effect to the appropriate handler method.""" result = None if isinstance(effect, AgentEffect): diff --git a/packages/doeff-agents/src/doeff_agents/handlers/production.py b/packages/doeff-agents/src/doeff_agents/handlers/production.py index 5273cf34f..404c8487e 100644 --- a/packages/doeff-agents/src/doeff_agents/handlers/production.py +++ b/packages/doeff-agents/src/doeff_agents/handlers/production.py @@ -279,7 +279,7 @@ def _run_agent_task(handler: AgentHandler, task: AgentTask) -> object: raise AssertionError("unreachable agent retry state") -def _validation_failure_from_outcome( +def _validation_failure_from_outcome( # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged outcome: AwaitOutcome, schema: dict[str, object], ) -> AgentValidationFailure | None: diff --git a/packages/doeff-agents/src/doeff_agents/mcp_server.py b/packages/doeff-agents/src/doeff_agents/mcp_server.py index cac9be619..9d46e9c66 100644 --- a/packages/doeff-agents/src/doeff_agents/mcp_server.py +++ b/packages/doeff-agents/src/doeff_agents/mcp_server.py @@ -35,10 +35,11 @@ import queue import threading import uuid +from collections.abc import Callable from dataclasses import dataclass -from http.server import HTTPServer, BaseHTTPRequestHandler +from http.server import BaseHTTPRequestHandler, HTTPServer from socketserver import ThreadingMixIn -from typing import Any, Callable +from typing import Any from urllib.parse import parse_qs, urlparse from doeff.mcp import McpToolDef @@ -85,7 +86,7 @@ class _McpHandler(BaseHTTPRequestHandler): server: McpToolServer # type narrowing # Suppress per-request logging - def log_message(self, format: str, *args: Any) -> None: + def log_message(self, format: str, *args: Any) -> None: # noqa: A002 - public API parameter name is intentionally stable log.debug("MCP %s", format % args) def do_GET(self) -> None: @@ -327,7 +328,7 @@ def dispatch_jsonrpc(self, msg: dict) -> dict | None: return _jsonrpc_error(msg_id, -32601, f"Method not found: {method}") - def _handle_tool_call(self, msg_id: Any, params: dict) -> dict: + def _handle_tool_call(self, msg_id: Any, params: dict) -> dict: # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged tool_name = params.get("name", "") arguments = params.get("arguments", {}) diff --git a/packages/doeff-agents/src/doeff_agents/result_validation.py b/packages/doeff-agents/src/doeff_agents/result_validation.py index ef94ace28..185b0ee27 100644 --- a/packages/doeff-agents/src/doeff_agents/result_validation.py +++ b/packages/doeff-agents/src/doeff_agents/result_validation.py @@ -18,7 +18,7 @@ def validate_result_payload(payload: Any, schema: Mapping[str, Any]) -> str | No return None -def _validate(instance: Any, schema: Mapping[str, Any], loc: str) -> None: +def _validate(instance: Any, schema: Mapping[str, Any], loc: str) -> None: # noqa: PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged if not isinstance(schema, Mapping): raise ValueError(f"schema at '{loc}' is not an object") @@ -97,7 +97,7 @@ def _validate(instance: Any, schema: Mapping[str, Any], loc: str) -> None: _validate(item, items, f"{loc}[{index}]") -def _matches_type(instance: Any, expected_type: Any) -> bool: +def _matches_type(instance: Any, expected_type: Any) -> bool: # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged if isinstance(expected_type, Sequence) and not isinstance(expected_type, (str, bytes)): return any(_matches_type(instance, item) for item in expected_type) if expected_type == "object": diff --git a/packages/doeff-agents/tests/test_l2_session_algebra.py b/packages/doeff-agents/tests/test_l2_session_algebra.py index d35a6d85f..043fb22bf 100644 --- a/packages/doeff-agents/tests/test_l2_session_algebra.py +++ b/packages/doeff-agents/tests/test_l2_session_algebra.py @@ -5,8 +5,6 @@ from pathlib import Path import pytest -from doeff import do, run - from doeff_agents import AgentType from doeff_agents.effects import ( AgentAttemptExhaustedError, @@ -18,9 +16,9 @@ AwaitStatus, FollowUp, FollowUpEffect, + L2SessionHandle, LaunchSession, LaunchSessionEffect, - L2SessionHandle, ReleaseSession, ReleaseSessionEffect, StopSession, @@ -29,6 +27,7 @@ ) from doeff_agents.handlers.testing import ScenarioAgentHandler, ScenarioStep +from doeff import do, run ARTIFACT_SCHEMA = { "type": "object", diff --git a/packages/doeff-agents/tests/test_mcp_server.py b/packages/doeff-agents/tests/test_mcp_server.py index 07b80a9df..c7e8c5c20 100644 --- a/packages/doeff-agents/tests/test_mcp_server.py +++ b/packages/doeff-agents/tests/test_mcp_server.py @@ -1,16 +1,13 @@ """Tests for MCP SSE server and Launch integration.""" -import json import http.client -import threading - -import pytest +import json -from doeff import do, run, WithHandler, Resume -from doeff.mcp import McpToolDef, McpParamSchema -from doeff_agents.mcp_server import McpToolServer from doeff_agents.handlers import _make_run_tool +from doeff_agents.mcp_server import McpToolServer +from doeff import Resume, do, run +from doeff.mcp import McpParamSchema, McpToolDef # -- Helpers ----------------------------------------------------------------- @@ -258,9 +255,10 @@ def test_make_run_tool_no_handlers(self): def test_make_run_tool_with_handler(self): """run_tool wraps program with captured handlers.""" - from doeff import EffectBase, do, Perform from dataclasses import dataclass + from doeff import EffectBase, Perform, do + @dataclass(frozen=True) class PrefixEffect(EffectBase): text: str diff --git a/packages/doeff-agents/tests/test_mcp_server_loop.py b/packages/doeff-agents/tests/test_mcp_server_loop.py index 78fd74595..7f197fdf1 100644 --- a/packages/doeff-agents/tests/test_mcp_server_loop.py +++ b/packages/doeff-agents/tests/test_mcp_server_loop.py @@ -10,19 +10,14 @@ from __future__ import annotations import threading -import time import hy # noqa: F401 (enable .hy imports) -import pytest - -from doeff import EffectBase, Pass, Perform, Pure, Resume, WithHandler, do, run -from doeff_core_effects.handlers import state -from doeff_core_effects.scheduler import Spawn, Wait, scheduled -from doeff.mcp import McpParamSchema, McpToolDef - from doeff_agents.handlers.mcp_server_loop import mcp_server_loop from doeff_agents.mcp_server import McpToolRequest, McpToolServer +from doeff_core_effects.scheduler import scheduled +from doeff import EffectBase, Pass, Perform, Pure, Resume, do, run +from doeff.mcp import McpParamSchema, McpToolDef # --------------------------------------------------------------------------- # Test fixtures @@ -114,7 +109,6 @@ def driver(): @do def main(): yield mcp_server_loop(server, []) - return None run(scheduled(main())) assert captured == [(True, "echo: hi")] @@ -141,7 +135,6 @@ def driver(): @do def main(): yield mcp_server_loop(server, full_stack) - return None run(scheduled(main())) assert driver_result == [(True, "Hello, World!")] @@ -167,7 +160,6 @@ def driver(): @do def main(): yield mcp_server_loop(server, []) - return None run(scheduled(main())) ok, value = driver_result[0] @@ -205,7 +197,6 @@ def driver(): @do def main(): yield mcp_server_loop(server, []) - return None run(scheduled(main())) ok, value = driver_result[0] @@ -239,7 +230,6 @@ def driver(): @do def main(): yield mcp_server_loop(server, []) - return None run(scheduled(main())) assert results == [(True, "echo: one"), (True, "echo: two")] @@ -259,6 +249,5 @@ def driver(): @do def main(): yield mcp_server_loop(server, []) - return None run(scheduled(main())) # should return, not hang diff --git a/packages/doeff-agents/tests/test_onboarding_dismiss.py b/packages/doeff-agents/tests/test_onboarding_dismiss.py index 56b599a5a..d52cf1c31 100644 --- a/packages/doeff-agents/tests/test_onboarding_dismiss.py +++ b/packages/doeff-agents/tests/test_onboarding_dismiss.py @@ -5,7 +5,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) -from doeff_agents.session import _dismiss_onboarding_dialogs # noqa: E402 +from doeff_agents.session import _dismiss_onboarding_dialogs class FakeBackend: @@ -35,8 +35,8 @@ def send_keys( def test_dismiss_onboarding_does_not_treat_theme_prompt_as_ready() -> None: backend = FakeBackend( [ - "Choose the text style that looks best with your terminal\n❯ 1. Dark mode", - "❯ Ready for input", + "Choose the text style that looks best with your terminal\n❯ 1. Dark mode", # noqa: RUF001 - test fixture intentionally matches the literal rendered glyph + "❯ Ready for input", # noqa: RUF001 - test fixture intentionally matches the literal rendered glyph ] ) diff --git a/packages/doeff-agents/tests/test_refactored_effects.py b/packages/doeff-agents/tests/test_refactored_effects.py index 0f69ef800..ad05ec792 100644 --- a/packages/doeff-agents/tests/test_refactored_effects.py +++ b/packages/doeff-agents/tests/test_refactored_effects.py @@ -2,8 +2,6 @@ from pathlib import Path -import pytest - from doeff_agents.adapters.base import AgentType @@ -31,9 +29,10 @@ def test_launch_effect_flat_fields(self): assert not hasattr(effect, "config"), "LaunchEffect must not have .config" def test_launch_effect_mcp_tools(self): - from doeff.mcp import McpParamSchema, McpToolDef from doeff_agents.effects.agent import LaunchEffect + from doeff.mcp import McpParamSchema, McpToolDef + tool = McpToolDef( name="test-tool", description="A test", From 569758427041480c58b9b0331c62f8ae1dde6a66 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:57 +0900 Subject: [PATCH 04/35] Clean Ruff violations in doeff-conductor --- .../examples/01_hello_workflow.py | 4 +-- .../examples/02_issue_lifecycle.py | 4 +-- .../examples/04_multi_agent.py | 3 +-- .../examples/05_custom_template.py | 4 +-- .../doeff-conductor/examples/06_api_usage.py | 2 +- .../src/doeff_conductor/dsl.py | 4 +-- .../src/doeff_conductor/effects/__init__.py | 6 ++--- .../src/doeff_conductor/effects/issue.py | 2 +- .../doeff_conductor/handlers/git_handler.py | 3 +-- .../doeff_conductor/handlers/issue_handler.py | 25 ++++++++---------- .../src/doeff_conductor/templates/basic_pr.py | 13 +++++++--- .../doeff_conductor/templates/enforced_pr.py | 13 +++++++--- .../doeff_conductor/templates/multi_agent.py | 11 ++++---- .../src/doeff_conductor/workflow_runtime.py | 2 +- packages/doeff-conductor/tests/conftest.py | 26 +++++++++---------- packages/doeff-conductor/tests/test_api.py | 8 +++--- .../tests/test_c8_single_file_run.py | 5 ++-- .../doeff-conductor/tests/test_effects.py | 2 +- .../tests/test_exec_handler.py | 2 +- .../doeff-conductor/tests/test_templates.py | 1 - packages/doeff-conductor/tests/test_types.py | 2 +- .../tests/test_workflow_e2e.py | 3 ++- 22 files changed, 75 insertions(+), 70 deletions(-) diff --git a/packages/doeff-conductor/examples/01_hello_workflow.py b/packages/doeff-conductor/examples/01_hello_workflow.py index 1b7340c48..153b9d9fc 100644 --- a/packages/doeff-conductor/examples/01_hello_workflow.py +++ b/packages/doeff-conductor/examples/01_hello_workflow.py @@ -1,11 +1,11 @@ #!/usr/bin/env python """Example 01: minimal workspace workflow.""" -from doeff import EffectGenerator, do - from doeff_conductor import Commit, CreateWorkspace, DeleteWorkspace, Exec, Workspace from doeff_conductor.handlers import mock_handlers, run_sync +from doeff import EffectGenerator, do + @do def hello_workflow() -> EffectGenerator[str]: diff --git a/packages/doeff-conductor/examples/02_issue_lifecycle.py b/packages/doeff-conductor/examples/02_issue_lifecycle.py index 9f887ea17..178e0ae55 100644 --- a/packages/doeff-conductor/examples/02_issue_lifecycle.py +++ b/packages/doeff-conductor/examples/02_issue_lifecycle.py @@ -1,11 +1,11 @@ #!/usr/bin/env python """Example 02: issue lifecycle.""" -from doeff import EffectGenerator, do - from doeff_conductor import CreateIssue, GetIssue, Issue, IssueStatus, ListIssues, ResolveIssue from doeff_conductor.handlers import mock_handlers, run_sync +from doeff import EffectGenerator, do + @do def issue_lifecycle_demo() -> EffectGenerator[Issue]: diff --git a/packages/doeff-conductor/examples/04_multi_agent.py b/packages/doeff-conductor/examples/04_multi_agent.py index ba58a6855..f2e4886a9 100644 --- a/packages/doeff-conductor/examples/04_multi_agent.py +++ b/packages/doeff-conductor/examples/04_multi_agent.py @@ -1,10 +1,9 @@ #!/usr/bin/env python """Example 04: multi-agent workflow with scheduler-backed parallelism.""" -from doeff_core_effects.scheduler import scheduled - from doeff_conductor import Issue, IssueStatus, multi_agent from doeff_conductor.handlers import mock_handlers, run_sync +from doeff_core_effects.scheduler import scheduled def main() -> None: diff --git a/packages/doeff-conductor/examples/05_custom_template.py b/packages/doeff-conductor/examples/05_custom_template.py index f7665554f..fb4e6453a 100644 --- a/packages/doeff-conductor/examples/05_custom_template.py +++ b/packages/doeff-conductor/examples/05_custom_template.py @@ -1,8 +1,6 @@ #!/usr/bin/env python """Example 05: custom PR workflow with deterministic Exec gates.""" -from doeff import EffectGenerator, do - from doeff_conductor import ( Agent, AgentTask, @@ -19,6 +17,8 @@ ) from doeff_conductor.handlers import mock_handlers, run_sync +from doeff import EffectGenerator, do + CUSTOM_TEMPLATE_SCHEMA = { "type": "object", "required": ["summary"], diff --git a/packages/doeff-conductor/examples/06_api_usage.py b/packages/doeff-conductor/examples/06_api_usage.py index 93377518e..559d818d6 100644 --- a/packages/doeff-conductor/examples/06_api_usage.py +++ b/packages/doeff-conductor/examples/06_api_usage.py @@ -64,7 +64,7 @@ def demo_api_workflow_management(): print(f"Using state directory: {state_dir}\n") # Create sample issue - issue = Issue( + Issue( id="ISSUE-API-001", title="Demo API workflow", body="This is a demo issue for API usage example", diff --git a/packages/doeff-conductor/src/doeff_conductor/dsl.py b/packages/doeff-conductor/src/doeff_conductor/dsl.py index 637e944b9..9beaca374 100644 --- a/packages/doeff-conductor/src/doeff_conductor/dsl.py +++ b/packages/doeff-conductor/src/doeff_conductor/dsl.py @@ -55,7 +55,7 @@ def _next_workspace_occurrence() -> int: workflow module, so identical source yields identical occurrence numbers across processes — the property resume stability needs. """ - global _WORKSPACE_OCCURRENCE + global _WORKSPACE_OCCURRENCE # noqa: PLW0603 - module-level counter is the existing state model value = _WORKSPACE_OCCURRENCE _WORKSPACE_OCCURRENCE += 1 return value @@ -66,7 +66,7 @@ def _next_workspace_occurrence() -> int: def reset_workspace_occurrences() -> None: """Reset the occurrence counter; called by the loader per module exec.""" - global _WORKSPACE_OCCURRENCE + global _WORKSPACE_OCCURRENCE # noqa: PLW0603 - module-level counter is the existing state model _WORKSPACE_OCCURRENCE = 0 diff --git a/packages/doeff-conductor/src/doeff_conductor/effects/__init__.py b/packages/doeff-conductor/src/doeff_conductor/effects/__init__.py index 566115eab..407f175d5 100644 --- a/packages/doeff-conductor/src/doeff_conductor/effects/__init__.py +++ b/packages/doeff-conductor/src/doeff_conductor/effects/__init__.py @@ -123,8 +123,6 @@ "Commit", # Base "ConductorEffectBase", - # Exec - "Exec", # Issue "CreateIssue", "CreatePR", @@ -133,6 +131,8 @@ "DefaultReviewRouter", "DeleteWorkspace", "DurableReviewBudget", + # Exec + "Exec", "GateCall", "GateOption", "GetIssue", @@ -144,8 +144,8 @@ "GitPushEffect", "ListIssues", "MergeCall", - "MergeWorkspaces", "MergePR", + "MergeWorkspaces", "OpenGate", "OpenGateReason", "Push", diff --git a/packages/doeff-conductor/src/doeff_conductor/effects/issue.py b/packages/doeff-conductor/src/doeff_conductor/effects/issue.py index ae6413775..26c78986c 100644 --- a/packages/doeff-conductor/src/doeff_conductor/effects/issue.py +++ b/packages/doeff-conductor/src/doeff_conductor/effects/issue.py @@ -14,7 +14,7 @@ from .base import ConductorEffectBase if TYPE_CHECKING: - from ..types import Issue, IssueStatus + from doeff_conductor.types import Issue, IssueStatus @dataclass(frozen=True, kw_only=True) diff --git a/packages/doeff-conductor/src/doeff_conductor/handlers/git_handler.py b/packages/doeff-conductor/src/doeff_conductor/handlers/git_handler.py index 03d897c5a..41fd471e5 100644 --- a/packages/doeff-conductor/src/doeff_conductor/handlers/git_handler.py +++ b/packages/doeff-conductor/src/doeff_conductor/handlers/git_handler.py @@ -23,9 +23,8 @@ from doeff_git.types import PRHandle as GitPRHandle from doeff_conductor.exceptions import GitCommandError -from doeff_conductor.types import MergeStrategy - from doeff_conductor.git_workspace import get_current_commit, run_git +from doeff_conductor.types import MergeStrategy if TYPE_CHECKING: from doeff_conductor.effects.git import Commit, CreatePR, MergePR, Push diff --git a/packages/doeff-conductor/src/doeff_conductor/handlers/issue_handler.py b/packages/doeff-conductor/src/doeff_conductor/handlers/issue_handler.py index 92b94b95a..eeea5561d 100644 --- a/packages/doeff-conductor/src/doeff_conductor/handlers/issue_handler.py +++ b/packages/doeff-conductor/src/doeff_conductor/handlers/issue_handler.py @@ -2,6 +2,7 @@ Issue handler for doeff-conductor. """ +import contextlib import re import secrets from datetime import datetime, timezone @@ -10,11 +11,11 @@ import yaml -from ..exceptions import IssueNotFoundError +from doeff_conductor.exceptions import IssueNotFoundError if TYPE_CHECKING: - from ..effects.issue import CreateIssue, GetIssue, ListIssues, ResolveIssue - from ..types import Issue + from doeff_conductor.effects.issue import CreateIssue, GetIssue, ListIssues, ResolveIssue + from doeff_conductor.types import Issue def _get_issues_dir() -> Path: @@ -89,7 +90,7 @@ def handle_create_issue(self, effect: "CreateIssue") -> "Issue": Creates a new issue file with YAML frontmatter. """ - from ..types import Issue, IssueStatus + from doeff_conductor.types import Issue, IssueStatus # Generate ID issue_id = _generate_issue_id() @@ -131,7 +132,7 @@ def handle_list_issues(self, effect: "ListIssues") -> "list[Issue]": Lists issues from the issues directory with optional filters. """ - from ..types import Issue, IssueStatus + from doeff_conductor.types import Issue, IssueStatus issues = [] @@ -153,18 +154,16 @@ def handle_list_issues(self, effect: "ListIssues") -> "list[Issue]": labels = tuple(frontmatter.get("labels", [])) # Apply labels filter (any match) - if effect.labels and not any(l in labels for l in effect.labels): + if effect.labels and not any(label in labels for label in effect.labels): continue # Parse dates created_at = datetime.now(timezone.utc) if frontmatter.get("created"): - try: + with contextlib.suppress(ValueError, TypeError): created_at = datetime.fromisoformat( str(frontmatter["created"]) ).replace(tzinfo=timezone.utc) - except (ValueError, TypeError): - pass issue = Issue( id=frontmatter["id"], @@ -199,7 +198,7 @@ def handle_get_issue(self, effect: "GetIssue") -> "Issue": Gets an issue by ID. """ - from ..types import Issue, IssueStatus + from doeff_conductor.types import Issue, IssueStatus file_path = self.issues_dir / f"{effect.id}.md" @@ -215,12 +214,10 @@ def handle_get_issue(self, effect: "GetIssue") -> "Issue": # Parse dates created_at = datetime.now(timezone.utc) if frontmatter.get("created"): - try: + with contextlib.suppress(ValueError, TypeError): created_at = datetime.fromisoformat(str(frontmatter["created"])).replace( tzinfo=timezone.utc ) - except (ValueError, TypeError): - pass return Issue( id=frontmatter["id"], @@ -242,7 +239,7 @@ def handle_resolve_issue(self, effect: "ResolveIssue") -> "Issue": Updates an issue's status to resolved. """ - from ..types import Issue, IssueStatus + from doeff_conductor.types import Issue, IssueStatus file_path = self.issues_dir / f"{effect.issue.id}.md" diff --git a/packages/doeff-conductor/src/doeff_conductor/templates/basic_pr.py b/packages/doeff-conductor/src/doeff_conductor/templates/basic_pr.py index 7d512aac2..d2d2b2637 100644 --- a/packages/doeff-conductor/src/doeff_conductor/templates/basic_pr.py +++ b/packages/doeff-conductor/src/doeff_conductor/templates/basic_pr.py @@ -11,8 +11,7 @@ """ from doeff import EffectGenerator, do - -from ..types import Issue, PRHandle +from doeff_conductor.types import Issue, PRHandle IMPLEMENT_SCHEMA = { "type": "object", @@ -34,7 +33,15 @@ def basic_pr(issue: Issue) -> EffectGenerator[PRHandle]: Returns: PRHandle for the created PR """ - from ..effects import Agent, AgentTask, Commit, CreatePR, CreateWorkspace, Push, ResolveIssue + from doeff_conductor.effects import ( + Agent, + AgentTask, + Commit, + CreatePR, + CreateWorkspace, + Push, + ResolveIssue, + ) # Step 1: Create isolated worktree (identity is resume-stable per issue) env = yield CreateWorkspace(issue=issue, workspace_id=f"{issue.id.lower()}-basic-pr") diff --git a/packages/doeff-conductor/src/doeff_conductor/templates/enforced_pr.py b/packages/doeff-conductor/src/doeff_conductor/templates/enforced_pr.py index 53dd25ced..c9ec735ac 100644 --- a/packages/doeff-conductor/src/doeff_conductor/templates/enforced_pr.py +++ b/packages/doeff-conductor/src/doeff_conductor/templates/enforced_pr.py @@ -13,8 +13,7 @@ from doeff import EffectGenerator, do - -from ..types import Issue, PRHandle +from doeff_conductor.types import Issue, PRHandle IMPLEMENT_SCHEMA = { "type": "object", @@ -55,7 +54,15 @@ def enforced_pr( Raises: RuntimeError: If tests still fail after max_retries """ - from ..effects import Agent, AgentTask, Commit, CreatePR, CreateWorkspace, Push, ResolveIssue + from doeff_conductor.effects import ( + Agent, + AgentTask, + Commit, + CreatePR, + CreateWorkspace, + Push, + ResolveIssue, + ) # Step 1: Create isolated worktree (identity is resume-stable per issue) env = yield CreateWorkspace(issue=issue, workspace_id=f"{issue.id.lower()}-enforced-pr") diff --git a/packages/doeff-conductor/src/doeff_conductor/templates/multi_agent.py b/packages/doeff-conductor/src/doeff_conductor/templates/multi_agent.py index 2d41efc34..cd3d51715 100644 --- a/packages/doeff-conductor/src/doeff_conductor/templates/multi_agent.py +++ b/packages/doeff-conductor/src/doeff_conductor/templates/multi_agent.py @@ -12,8 +12,7 @@ """ from doeff import EffectGenerator, Gather, Spawn, do - -from ..types import Issue, PRHandle, Workspace +from doeff_conductor.types import Issue, PRHandle, Workspace ARTIFACT_SCHEMA = { "type": "object", @@ -29,7 +28,7 @@ @do def _create_workspace(issue: Issue, suffix: str) -> EffectGenerator[Workspace]: """Create a worktree (wrapper for Spawn compatibility).""" - from ..effects import CreateWorkspace + from doeff_conductor.effects import CreateWorkspace return ( yield CreateWorkspace(issue=issue, workspace_id=f"{issue.id.lower()}-{suffix}") ) @@ -43,7 +42,7 @@ def _run_agent( node_id: str, ) -> EffectGenerator[dict]: """Run an agent (wrapper for Spawn compatibility).""" - from ..effects import Agent, AgentTask + from doeff_conductor.effects import Agent, AgentTask return ( yield Agent( AgentTask( @@ -63,7 +62,7 @@ def _run_agent( @do def _commit(env: Workspace, message: str) -> EffectGenerator[str]: """Create a commit (wrapper for Spawn compatibility).""" - from ..effects import Commit + from doeff_conductor.effects import Commit return (yield Commit(workspace=env, message=message)) @@ -77,7 +76,7 @@ def multi_agent(issue: Issue) -> EffectGenerator[PRHandle]: Returns: PRHandle for the created PR """ - from ..effects import ( + from doeff_conductor.effects import ( Agent, AgentTask, Commit, diff --git a/packages/doeff-conductor/src/doeff_conductor/workflow_runtime.py b/packages/doeff-conductor/src/doeff_conductor/workflow_runtime.py index 180a7aab3..dff4cc72c 100644 --- a/packages/doeff-conductor/src/doeff_conductor/workflow_runtime.py +++ b/packages/doeff-conductor/src/doeff_conductor/workflow_runtime.py @@ -667,7 +667,7 @@ def _workspace_identity(run_id: str, node_key: str) -> str: re-adopted the first run's branch including its commits (review finding F1, reproduced live). """ - digest: str = hashlib.sha256(f"{run_id}\n{node_key}".encode("utf-8")).hexdigest()[:8] + digest: str = hashlib.sha256(f"{run_id}\n{node_key}".encode()).hexdigest()[:8] slug: str = _slugify(node_key)[-48:].strip("-") return f"{_slugify(run_id)}-{slug}-{digest}" diff --git a/packages/doeff-conductor/tests/conftest.py b/packages/doeff-conductor/tests/conftest.py index 04a7eb083..de46c37ff 100644 --- a/packages/doeff-conductor/tests/conftest.py +++ b/packages/doeff-conductor/tests/conftest.py @@ -253,23 +253,21 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item """Auto-skip tests based on markers and environment.""" for item in items: # Skip requires_opencode tests if OpenCode not available - if "requires_opencode" in item.keywords: - if not is_opencode_available(): - item.add_marker( - pytest.mark.skip(reason="OpenCode not available. Start server or install CLI.") - ) + if "requires_opencode" in item.keywords and not is_opencode_available(): + item.add_marker( + pytest.mark.skip(reason="OpenCode not available. Start server or install CLI.") + ) # Skip e2e tests if not enabled (unless running with -m e2e) - if "e2e" in item.keywords: - if not is_e2e_enabled(): - # Check if running with explicit e2e marker - markexpr = config.getoption("-m", default=None) - if markexpr is None or "e2e" not in str(markexpr): - item.add_marker( - pytest.mark.skip( - reason="E2E tests disabled. Set CONDUCTOR_E2E=1 or use -m e2e" - ) + if "e2e" in item.keywords and not is_e2e_enabled(): + # Check if running with explicit e2e marker + markexpr = config.getoption("-m", default=None) + if markexpr is None or "e2e" not in str(markexpr): + item.add_marker( + pytest.mark.skip( + reason="E2E tests disabled. Set CONDUCTOR_E2E=1 or use -m e2e" ) + ) __all__ = [ diff --git a/packages/doeff-conductor/tests/test_api.py b/packages/doeff-conductor/tests/test_api.py index 50409e825..ecf2bb260 100644 --- a/packages/doeff-conductor/tests/test_api.py +++ b/packages/doeff-conductor/tests/test_api.py @@ -226,7 +226,7 @@ def test_get_workflow_ambiguous_prefix(self, api: ConductorAPI): "updated_at": now.isoformat(), })) - with pytest.raises(ValueError) as exc_info: + with pytest.raises(ValueError, match="Ambiguous") as exc_info: api.get_workflow("abc") assert "Ambiguous" in str(exc_info.value) @@ -272,7 +272,7 @@ def test_stop_specific_agent(self, api: ConductorAPI, running_workflow: str): def test_stop_nonexistent_workflow(self, api: ConductorAPI): """stop_workflow raises error for non-existent workflow.""" - with pytest.raises(ValueError) as exc_info: + with pytest.raises(ValueError, match="not found") as exc_info: api.stop_workflow("nonexistent") assert "not found" in str(exc_info.value) @@ -393,7 +393,7 @@ def test_run_workflow_invalid_template(self, api: ConductorAPI): def test_run_workflow_file_not_found(self, api: ConductorAPI): """run_workflow raises error for non-existent file.""" - with pytest.raises(ValueError) as exc_info: + with pytest.raises(ValueError, match="not found") as exc_info: api.run_workflow("/nonexistent/workflow.hy") assert "not found" in str(exc_info.value) @@ -554,7 +554,7 @@ def __init__(self) -> None: self.value = SimpleNamespace(url="inner-url") self.url = "outer-url" - monkeypatch.setattr("doeff.run", lambda *args, **kwargs: NonRunResultWithValue()) + monkeypatch.setattr("doeff.run", lambda *_args, **_kwargs: NonRunResultWithValue()) handle = api.run_workflow(str(workflow_file)) diff --git a/packages/doeff-conductor/tests/test_c8_single_file_run.py b/packages/doeff-conductor/tests/test_c8_single_file_run.py index 6672432c9..da4848fdf 100644 --- a/packages/doeff-conductor/tests/test_c8_single_file_run.py +++ b/packages/doeff-conductor/tests/test_c8_single_file_run.py @@ -6,14 +6,13 @@ import pytest from click.testing import CliRunner -from doeff_conductor.effects import AgentTask -from doeff_conductor.replay_keying import ResolvedIdentity from doeff_agents.effects.agent import AgentAttemptExhaustedError from doeff_conductor.api import ConductorAPI from doeff_conductor.cli import cli -from doeff_conductor.effects import AgentEffect +from doeff_conductor.effects import AgentEffect, AgentTask from doeff_conductor.handlers.journaled_agent import JournaledAgentHandler from doeff_conductor.handlers.testing import MockConductorRuntime, mock_handlers +from doeff_conductor.replay_keying import ResolvedIdentity from doeff_conductor.types import WorkflowStatus from doeff_conductor.workflow_loader import ( WorkflowNondeterminismError, diff --git a/packages/doeff-conductor/tests/test_effects.py b/packages/doeff-conductor/tests/test_effects.py index 43edfcf49..575767160 100644 --- a/packages/doeff-conductor/tests/test_effects.py +++ b/packages/doeff-conductor/tests/test_effects.py @@ -12,8 +12,8 @@ DeleteWorkspace, GetIssue, ListIssues, - MergeWorkspaces, MergePR, + MergeWorkspaces, Push, ResolveIssue, ) diff --git a/packages/doeff-conductor/tests/test_exec_handler.py b/packages/doeff-conductor/tests/test_exec_handler.py index 55ef7941a..d46ac01f1 100644 --- a/packages/doeff-conductor/tests/test_exec_handler.py +++ b/packages/doeff-conductor/tests/test_exec_handler.py @@ -53,7 +53,7 @@ def test_exec_runs_against_workspace_via_resolver(tmp_path: Path) -> None: (workspace_path / "input.txt").write_text("workspace-data\n") workspace = Workspace(id="ws-1", repo="default", ref="feature", base_ref="main") handler = ExecHandler( - workspace_resolver=lambda candidate: workspace_path, + workspace_resolver=lambda _candidate: workspace_path, log_dir=tmp_path / "logs", ) diff --git a/packages/doeff-conductor/tests/test_templates.py b/packages/doeff-conductor/tests/test_templates.py index 57e6fdd99..666599f29 100644 --- a/packages/doeff-conductor/tests/test_templates.py +++ b/packages/doeff-conductor/tests/test_templates.py @@ -33,7 +33,6 @@ Workspace, ) - HandlerMap = dict[type[Any], object] diff --git a/packages/doeff-conductor/tests/test_types.py b/packages/doeff-conductor/tests/test_types.py index 53788990a..f4132f8c5 100644 --- a/packages/doeff-conductor/tests/test_types.py +++ b/packages/doeff-conductor/tests/test_types.py @@ -11,9 +11,9 @@ MergeStrategy, MergeWorkspacesResult, PRHandle, - Workspace, WorkflowHandle, WorkflowStatus, + Workspace, ) diff --git a/packages/doeff-conductor/tests/test_workflow_e2e.py b/packages/doeff-conductor/tests/test_workflow_e2e.py index 3134a1674..110bd7b48 100644 --- a/packages/doeff-conductor/tests/test_workflow_e2e.py +++ b/packages/doeff-conductor/tests/test_workflow_e2e.py @@ -226,10 +226,11 @@ def test_template_imports(self): assert callable(func) def test_basic_pr_template_structure(self): - from doeff import ProgramBase from doeff_conductor.templates import basic_pr from doeff_conductor.types import Issue + from doeff import ProgramBase + issue = Issue( id="TEST-001", title="Test Feature", From 7bdb1b590122d24368dace9cf56b0e013887a3c3 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:57 +0900 Subject: [PATCH 05/35] Clean Ruff violations in doeff-core-effects --- .../doeff_core_effects/cache.py | 13 ++-- .../doeff_core_effects/cache_effects.py | 3 +- .../doeff_core_effects/cache_handlers.py | 36 +++++----- .../doeff_core_effects/handlers.py | 66 ++++++++++++------- .../doeff_core_effects/memo_effects.py | 11 ++-- .../doeff_core_effects/memo_handlers.py | 39 ++++++----- .../doeff_core_effects/scheduler.py | 45 +++++++------ .../doeff_core_effects/storage/__init__.py | 8 ++- 8 files changed, 121 insertions(+), 100 deletions(-) diff --git a/packages/doeff-core-effects/doeff_core_effects/cache.py b/packages/doeff-core-effects/doeff_core_effects/cache.py index fcf69308a..2b2556e19 100644 --- a/packages/doeff-core-effects/doeff_core_effects/cache.py +++ b/packages/doeff-core-effects/doeff_core_effects/cache.py @@ -9,13 +9,16 @@ @cache decorator. """ +import contextlib import os import tempfile from collections.abc import Callable, Mapping from pathlib import Path from typing import Any, TypeVar -from frozendict import frozendict as FrozenDict +from frozendict import ( + frozendict as FrozenDict, # noqa: N812 - public FrozenDict alias is intentionally stable +) from doeff import UnhandledEffect, do from doeff_core_effects.memo_effects import MemoExists, MemoGet, MemoPut @@ -131,20 +134,16 @@ def cached_fn(*args, **kwargs): # Best-effort store: if no memo handler is installed, swallow. The # compute result returned above is authoritative. - try: + with contextlib.suppress(UnhandledEffect): yield MemoPut(cache_key_obj, result, policy=memo_policy) - except UnhandledEffect: - pass return result # Preserve metadata for attr in ("__name__", "__qualname__", "__doc__", "__module__"): val = getattr(func, attr, None) if val is not None: - try: + with contextlib.suppress(AttributeError, TypeError): setattr(cached_fn, attr, val) - except (AttributeError, TypeError): - pass return cached_fn diff --git a/packages/doeff-core-effects/doeff_core_effects/cache_effects.py b/packages/doeff-core-effects/doeff_core_effects/cache_effects.py index fa9a1773a..aac6178aa 100644 --- a/packages/doeff-core-effects/doeff_core_effects/cache_effects.py +++ b/packages/doeff-core-effects/doeff_core_effects/cache_effects.py @@ -3,13 +3,14 @@ from collections.abc import Mapping from typing import Any +from doeff_vm import EffectBase + from doeff_core_effects.cache_policy import ( CacheLifecycle, CachePolicy, CacheStorage, ensure_cache_policy, ) -from doeff_vm import EffectBase class CacheGetEffect(EffectBase): 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 61b7851b9..5489d3c38 100644 --- a/packages/doeff-core-effects/doeff_core_effects/cache_handlers.py +++ b/packages/doeff-core-effects/doeff_core_effects/cache_handlers.py @@ -5,20 +5,18 @@ import json from collections.abc import Callable, Mapping, Set from pathlib import Path -from typing import Any, TypeAlias +from typing import TypeAlias from doeff import do -from doeff.program import Resume, Pass - +from doeff.program import Pass, Resume from doeff_core_effects.cache_effects import ( + CacheExists, CacheExistsEffect, - CacheGetEffect, - CachePutEffect, CacheGet, + CacheGetEffect, CachePut, - CacheExists, + CachePutEffect, ) -from doeff_core_effects.effects import Try from doeff_core_effects.storage import DurableStorage, InMemoryStorage, SQLiteStorage MemoKeyFn: TypeAlias = Callable[[object], str] @@ -33,7 +31,7 @@ def _dumps(value: object) -> bytes: return serializer.dumps(value) -def _normalize_for_hash(value: object) -> object: +def _normalize_for_hash(value: object) -> object: # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged if dataclasses.is_dataclass(value) and not isinstance(value, type): return { "__type__": f"{type(value).__module__}.{type(value).__qualname__}", @@ -42,27 +40,27 @@ def _normalize_for_hash(value: object) -> object: for field in dataclasses.fields(value) }, } - elif isinstance(value, Mapping): + if isinstance(value, Mapping): return { str(key): _normalize_for_hash(item) for key, item in sorted(value.items(), key=lambda item: str(item[0])) } - elif isinstance(value, tuple): + if isinstance(value, tuple): return {"__tuple__": [_normalize_for_hash(item) for item in value]} - elif isinstance(value, list): + if isinstance(value, list): return [_normalize_for_hash(item) for item in value] - elif isinstance(value, Set) and not isinstance(value, (str, bytes, bytearray)): + if isinstance(value, Set) and not isinstance(value, (str, bytes, bytearray)): normalized = [_normalize_for_hash(item) for item in value] return { "__set__": sorted(normalized, key=lambda item: json.dumps(item, sort_keys=True)) } - elif isinstance(value, Path): + if isinstance(value, Path): return {"__path__": str(value)} - elif isinstance(value, bytes): + if isinstance(value, bytes): return {"__bytes__": value.hex()} - elif isinstance(value, type): + if isinstance(value, type): return f"{value.__module__}.{value.__qualname__}" - elif hasattr(value, "__dict__") and not isinstance(value, type): + if hasattr(value, "__dict__") and not isinstance(value, type): # Generic object with __dict__ (e.g. EffectBase subclasses) return { "__type__": f"{type(value).__module__}.{type(value).__qualname__}", @@ -177,14 +175,14 @@ def handler(effect, k): # Check cache — narrow try/except to CacheGet only. # Resume(k) must NOT be inside try/except: if the resumed body raises # KeyError, it must propagate, not fall through to the MISS path. - _MISS = object() # sentinel + miss_sentinel = object() # sentinel if (yield CacheExists(key)): try: cached = yield CacheGet(key) except KeyError: - cached = _MISS # race: evicted between Exists and Get + cached = miss_sentinel # race: evicted between Exists and Get - if cached is not _MISS: + if cached is not miss_sentinel: yield Slog(f"[memo] HIT {effect_type.__name__} key={key[:16]}…") result = yield Resume(k, cached) return result diff --git a/packages/doeff-core-effects/doeff_core_effects/handlers.py b/packages/doeff-core-effects/doeff_core_effects/handlers.py index 9f300e8a8..84c8b95c8 100644 --- a/packages/doeff-core-effects/doeff_core_effects/handlers.py +++ b/packages/doeff-core-effects/doeff_core_effects/handlers.py @@ -10,11 +10,17 @@ """ from doeff import do -from doeff.program import Resume, Pass - +from doeff.program import Pass, Resume from doeff_core_effects.effects import ( - Ask, Get, Put, Tell, Try, Slog, WriterTellEffect, - Local, Listen, Await, + Ask, + Await, + Get, + Listen, + Local, + Put, + Slog, + Try, + WriterTellEffect, ) @@ -95,9 +101,12 @@ def try_handler(effect, k): result = yield Try(some_program) # Ok(value) or Err(error) """ if isinstance(effect, Try): - from doeff_vm import Ok, Err - from doeff.program import WithHandler as WH + 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) @@ -156,8 +165,10 @@ def local_handler(effect, k): WithHandler(local_handler, body) """ if isinstance(effect, Local): - from doeff.program import WithHandler as WH 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 @@ -194,8 +205,10 @@ def listen_handler(effect, k): WithHandler(listen_handler, body) """ if isinstance(effect, Listen): - from doeff.program import WithHandler as WH 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,) @@ -224,10 +237,11 @@ def await_handler(): Uses ExternalPromise to bridge async into the scheduler. Requires scheduler to be installed. """ - from doeff_core_effects.scheduler import CreateExternalPromise, Wait import asyncio import threading + from doeff_core_effects.scheduler import CreateExternalPromise, Wait + # Shared event loop running in a background thread _loop = [None] _lock = threading.Lock() @@ -279,7 +293,7 @@ def _missing_key_message(key): ) -def lazy_ask(env=None, *, strict=False): +def lazy_ask(env=None, *, strict=False): # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged """Lazy Ask handler — replaces reader per SPEC-EFF-001. Handles Ask, Local, and lazy program evaluation. Takes env directly — @@ -312,11 +326,16 @@ def lazy_ask(env=None, *, strict=False): if env is None: env = {} - from doeff.program import Expand, ResumeThrow, WithHandler as WH 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 ( - CreateSemaphore, AcquireSemaphore, ReleaseSemaphore, + AcquireSemaphore, + CreateSemaphore, + ReleaseSemaphore, ) shared_cache = {} # key → value (override-independent entries) @@ -324,7 +343,7 @@ def lazy_ask(env=None, *, strict=False): eval_stack = [] # stack of dep-tracking sets for nested evals sems = {} # key → Semaphore handle - def _make_handler(effective_env, override_keys=frozenset()): + def _make_handler(effective_env, override_keys=frozenset()): # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged """Create a handler with the given effective env (base + overrides).""" scope_cache = {} # key → value (override-dependent, isolated per scope) scope_deps = {} # key → frozenset of dep keys @@ -349,7 +368,7 @@ def _cache_store(key, value, deps): shared_deps[key] = deps @do - def handler(effect, k): + def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, Ask): # Track as dependency if inside a lazy evaluation if eval_stack: @@ -478,12 +497,17 @@ def env_var_ask(*, prefix="DOEFF_"): always flow through to outer handlers (or Unhandled). """ import os + from doeff import Program - from doeff.program import WithHandler as WH - from doeff.handler_utils import get_inner_handlers 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, ReleaseSemaphore, + AcquireSemaphore, + CreateSemaphore, + ReleaseSemaphore, ) # cache[key] = (raw_env_value, resolved_value) @@ -491,7 +515,7 @@ def env_var_ask(*, prefix="DOEFF_"): sems: dict = {} @do - def handler(effect, k): + def handler(effect, k): # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged if not isinstance(effect, Ask): yield Pass(effect, k) return @@ -537,10 +561,7 @@ def handler(effect, k): maybe_program = value() except TypeError: maybe_program = value - if isinstance(maybe_program, Program): - value = maybe_program - else: - value = maybe_program # keep the callable's return value + value = maybe_program if isinstance(value, Program): inner_hs = yield get_inner_handlers(k) wrapped = value @@ -560,4 +581,3 @@ def handler(effect, k): return handler - diff --git a/packages/doeff-core-effects/doeff_core_effects/memo_effects.py b/packages/doeff-core-effects/doeff_core_effects/memo_effects.py index 703d43886..55ba68c3a 100644 --- a/packages/doeff-core-effects/doeff_core_effects/memo_effects.py +++ b/packages/doeff-core-effects/doeff_core_effects/memo_effects.py @@ -7,13 +7,14 @@ from collections.abc import Mapping from typing import Any +from doeff_vm import EffectBase + from doeff_core_effects.memo_policy import ( Lifecycle, MemoPolicy, RecomputeCost, ensure_memo_policy, ) -from doeff_vm import EffectBase class MemoGetEffect(EffectBase): @@ -66,13 +67,13 @@ def __repr__(self): # Convenience constructors -def MemoGet(key: Any, *, recompute_cost: RecomputeCost | str = RecomputeCost.CHEAP) -> MemoGetEffect: +def MemoGet(key: Any, *, recompute_cost: RecomputeCost | str = RecomputeCost.CHEAP) -> MemoGetEffect: # noqa: N802 - public or spec test name is intentionally stable if isinstance(recompute_cost, str): recompute_cost = RecomputeCost(recompute_cost) return MemoGetEffect(key=key, recompute_cost=recompute_cost) -def MemoPut( +def MemoPut( # noqa: N802 - public or spec test name is intentionally stable key: Any, value: Any, ttl: float | None = None, @@ -93,13 +94,13 @@ def MemoPut( return MemoPutEffect(key=key, value=value, policy=memo_policy, source_effect=source_effect) -def MemoDelete(key: Any, *, recompute_cost: RecomputeCost | str = RecomputeCost.CHEAP) -> MemoDeleteEffect: +def MemoDelete(key: Any, *, recompute_cost: RecomputeCost | str = RecomputeCost.CHEAP) -> MemoDeleteEffect: # noqa: N802 - public or spec test name is intentionally stable if isinstance(recompute_cost, str): recompute_cost = RecomputeCost(recompute_cost) return MemoDeleteEffect(key=key, recompute_cost=recompute_cost) -def MemoExists(key: Any, *, recompute_cost: RecomputeCost | str = RecomputeCost.CHEAP) -> MemoExistsEffect: +def MemoExists(key: Any, *, recompute_cost: RecomputeCost | str = RecomputeCost.CHEAP) -> MemoExistsEffect: # noqa: N802 - public or spec test name is intentionally stable if isinstance(recompute_cost, str): recompute_cost = RecomputeCost(recompute_cost) return MemoExistsEffect(key=key, recompute_cost=recompute_cost) 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 347c4ec6d..7ac20bd51 100644 --- a/packages/doeff-core-effects/doeff_core_effects/memo_handlers.py +++ b/packages/doeff-core-effects/doeff_core_effects/memo_handlers.py @@ -7,6 +7,7 @@ from __future__ import annotations +import contextlib import dataclasses import hashlib import json @@ -60,7 +61,7 @@ def _dumps(value: object) -> bytes: return serializer.dumps(value) -def _normalize_for_hash(value: object) -> object: +def _normalize_for_hash(value: object) -> object: # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged if dataclasses.is_dataclass(value) and not isinstance(value, type): return { "__type__": f"{type(value).__module__}.{type(value).__qualname__}", @@ -69,27 +70,27 @@ def _normalize_for_hash(value: object) -> object: for field in dataclasses.fields(value) }, } - elif isinstance(value, Mapping): + if isinstance(value, Mapping): return { str(key): _normalize_for_hash(item) for key, item in sorted(value.items(), key=lambda item: str(item[0])) } - elif isinstance(value, tuple): + if isinstance(value, tuple): return {"__tuple__": [_normalize_for_hash(item) for item in value]} - elif isinstance(value, list): + if isinstance(value, list): return [_normalize_for_hash(item) for item in value] - elif isinstance(value, Set) and not isinstance(value, (str, bytes, bytearray)): + if isinstance(value, Set) and not isinstance(value, (str, bytes, bytearray)): normalized = [_normalize_for_hash(item) for item in value] return { "__set__": sorted(normalized, key=lambda item: json.dumps(item, sort_keys=True)) } - elif isinstance(value, Path): + if isinstance(value, Path): return {"__path__": str(value)} - elif isinstance(value, bytes): + if isinstance(value, bytes): return {"__bytes__": value.hex()} - elif isinstance(value, type): + if isinstance(value, type): return f"{value.__module__}.{value.__qualname__}" - elif hasattr(value, "__dict__") and not isinstance(value, type): + if hasattr(value, "__dict__") and not isinstance(value, type): return { "__type__": f"{type(value).__module__}.{type(value).__qualname__}", **{ @@ -167,7 +168,7 @@ def memo_handler( from doeff_core_effects.effects import WriterTellEffect as Slog @do - def handler(effect, k): + def handler(effect, k): # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged if not isinstance(effect, (MemoGetEffect, MemoExistsEffect, MemoPutEffect)): yield Pass(effect, k) return @@ -207,8 +208,8 @@ def handler(effect, k): yield Slog(f"[memo-layer:{label}] MISS key={key[:16]}... → re-performing") try: outer_value = yield effect - except UnhandledEffect: - raise KeyError(effect.key) + except UnhandledEffect as exc: + raise KeyError(effect.key) from exc yield storage.put(key, outer_value) yield Slog(f"[memo-layer:{label}] WRITE-THROUGH key={key[:16]}...") result = yield Resume(k, outer_value) @@ -221,10 +222,8 @@ def handler(effect, k): # when all storage layers successfully stored. yield storage.put(key, effect.value) yield Slog(f"[memo-layer:{label}] PUT key={key[:16]}...") - try: + with contextlib.suppress(UnhandledEffect): yield effect # re-perform → outer handlers also store - except UnhandledEffect: - pass # no further outer = broadcast complete result = yield Resume(k, None) return result @@ -275,8 +274,8 @@ def handler(effect, k): key = key_fn(effect) yield Slog(f"[memo] checking {effect_type.__name__} key={key[:16]}...") - _MISS = object() - cached = _MISS + miss_sentinel = object() + cached = miss_sentinel # Memo storage absent or reports miss -> fall through to compute. The # yield effect compute path below is intentionally NOT wrapped: an # UnhandledEffect there is a real bug (no handler can produce the @@ -286,11 +285,11 @@ def handler(effect, k): try: cached = yield MemoGet(key, recompute_cost=recompute_cost) except KeyError: - cached = _MISS + cached = miss_sentinel except UnhandledEffect: - cached = _MISS + cached = miss_sentinel - if cached is not _MISS: + if cached is not miss_sentinel: yield Slog(f"[memo] HIT {effect_type.__name__} key={key[:16]}...") result = yield Resume(k, cached) return result diff --git a/packages/doeff-core-effects/doeff_core_effects/scheduler.py b/packages/doeff-core-effects/doeff_core_effects/scheduler.py index 102d8c8a9..df193cf99 100644 --- a/packages/doeff-core-effects/doeff_core_effects/scheduler.py +++ b/packages/doeff-core-effects/doeff_core_effects/scheduler.py @@ -21,10 +21,11 @@ def main(): run(scheduled(main())) """ -from doeff_vm import EffectBase, Ok, Err, TailEval +from doeff_vm import EffectBase, Err, Ok, TailEval + from doeff.do import do -from doeff.program import Pure, Resume, Transfer, Pass, Perform, WithHandler from doeff.handler_utils import get_inner_handlers +from doeff.program import Pass, Perform, Pure, Resume, Transfer, WithHandler def _enrich_exception_traceback(exc, task_meta=None, vm_ctx=None): @@ -46,13 +47,13 @@ def _enrich_exception_traceback(exc, task_meta=None, vm_ctx=None): if tb is not None: for fs in tb_mod.extract_tb(tb): fn = fs.filename - if any(p in fn for p in ('/doeff_vm/', '/doeff/do.py', '/doeff/run.py', - '/doeff_core_effects/')): + if any(p in fn for p in ("/doeff_vm/", "/doeff/do.py", "/doeff/run.py", + "/doeff_core_effects/")): continue entries.append(["frame", fs.name, fs.filename, fs.lineno]) if entries: - existing = getattr(exc, '__doeff_traceback__', None) or [] + existing = getattr(exc, "__doeff_traceback__", None) or [] exc.__doeff_traceback__ = entries + existing @@ -102,7 +103,6 @@ def __init__(self, task): class TaskCancelledError(Exception): """Raised when waiting on a cancelled task.""" - pass class Race(EffectBase): @@ -219,10 +219,10 @@ def __repr__(self): # Scheduler # --------------------------------------------------------------------------- -def scheduled(body_program): +def scheduled(body_program): # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged """Wrap a program with the scheduler. Returns a DoExpr.""" - import queue as queue_mod import heapq + import queue as queue_mod # --- State --- next_id = [0] @@ -232,7 +232,6 @@ def scheduled(body_program): semaphores = {} # sid → {permits, max_permits, waiters: deque of k} waiters = {} # waitable_key → [(type, k, ...)] or gather-state refs ready = [] # heapq: (-priority, seq, entry) - cancel_requested = set() # task ids pending cancellation external_queue = queue_mod.Queue() # thread-safe, blocking get() def fresh_id(): @@ -244,7 +243,7 @@ def waitable_key(obj): """Convert Task or Future to a dict key.""" if isinstance(obj, Task): return ("task", obj.task_id) - elif isinstance(obj, Future): + if isinstance(obj, Future): return ("promise", obj.promise_id) raise TypeError(f"expected Task or Future, got {type(obj).__name__}") @@ -252,7 +251,7 @@ def waitable_status(key): kind, wid = key if kind == "task": return tasks[wid]["status"], tasks[wid].get("result") - elif kind == "promise": + if kind == "promise": return promises[wid]["status"], promises[wid].get("result") return "unknown", None @@ -326,23 +325,23 @@ def pick_next(): for h in tasks[tid].pop("inner_handlers", []): prog = WithHandler(h, prog) return WithHandler(handler, wrap_task(tid, prog)) - elif entry[0] == "resume": + if entry[0] == "resume": _, cont, value = entry return Transfer(cont, value) - elif entry[0] == "raise": + if entry[0] == "raise": _, cont, error = entry return ResumeThrow(cont, error) - elif entry[0] == "wait_external": + if entry[0] == "wait_external": # Task waiting for external promise at NORMAL priority. # Keeps IDLE tasks (clock driver) from running. _, cont, wk = entry - if waitable_status(wk)[0] in TERMINAL: + if waitable_status(wk)[0] in terminal_statuses: resume_with_waitable_result(cont, wk) continue # Not yet resolved — block for one completion, drain rest _drain_one_external() drain() - if waitable_status(wk)[0] in TERMINAL: + if waitable_status(wk)[0] in terminal_statuses: resume_with_waitable_result(cont, wk) else: enqueue(entry, PRIORITY_EXTERNAL_WAIT) @@ -352,7 +351,7 @@ def pick_next(): # All tasks blocked — block for one external completion _drain_one_external() - TERMINAL = ("completed", "failed", "cancelled") + terminal_statuses = ("completed", "failed", "cancelled") def _release_task_refs(tid): """Drop heavy references from a terminal task. @@ -447,7 +446,7 @@ def drain(): wake_waiters(("promise", pid)) @do - def handler(effect, k): + def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged drain() if isinstance(effect, Spawn): # Capture inner handlers from continuation (between yield site and scheduler). @@ -471,14 +470,14 @@ def handler(effect, k): elif isinstance(effect, TaskCompleted): tid = effect.task_id r = effect.result - if hasattr(r, 'is_ok') and r.is_ok(): + if hasattr(r, "is_ok") and r.is_ok(): tasks[tid]["status"] = "completed" tasks[tid]["result"] = r.value else: tasks[tid]["status"] = "failed" - error = r.error if hasattr(r, 'error') else r + error = r.error if hasattr(r, "error") else r # Add spawn boundary to traceback - if isinstance(error, BaseException) and hasattr(error, '__doeff_traceback__'): + if isinstance(error, BaseException) and hasattr(error, "__doeff_traceback__"): error.__doeff_traceback__.insert(0, { "kind": "spawn_boundary", "task_id": tid, @@ -531,7 +530,7 @@ def handler(effect, k): if s == "cancelled": from doeff.program import ResumeThrow return (yield ResumeThrow(k, TaskCancelledError())) - if s not in TERMINAL: + if s not in terminal_statuses: pending_wks.append(wk) if not pending_wks: @@ -565,7 +564,7 @@ def handler(effect, k): # All pending for t in effect.tasks: wk = waitable_key(t) - if waitable_status(wk)[0] not in TERMINAL: + if waitable_status(wk)[0] not in terminal_statuses: waiters.setdefault(wk, []).append(("race", k, effect.tasks)) break yield TailEval(pick_next()) diff --git a/packages/doeff-core-effects/doeff_core_effects/storage/__init__.py b/packages/doeff-core-effects/doeff_core_effects/storage/__init__.py index 72c255f37..20d5707d4 100644 --- a/packages/doeff-core-effects/doeff_core_effects/storage/__init__.py +++ b/packages/doeff-core-effects/doeff_core_effects/storage/__init__.py @@ -69,8 +69,12 @@ def clear(self) -> None: # Import implementations for convenience -from doeff_core_effects.storage.memory import InMemoryStorage -from doeff_core_effects.storage.sqlite import SQLiteStorage +from doeff_core_effects.storage.memory import ( # noqa: E402 - late import preserves existing import/setup order + InMemoryStorage, +) +from doeff_core_effects.storage.sqlite import ( # noqa: E402 - late import preserves existing import/setup order + SQLiteStorage, +) __all__ = [ "DurableStorage", From 20e84e641779fd65a9e10e1b565a5a6a233c1618 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:57 +0900 Subject: [PATCH 06/35] Clean Ruff violations in doeff-docker --- packages/doeff-docker/tests/test_effects.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/doeff-docker/tests/test_effects.py b/packages/doeff-docker/tests/test_effects.py index bd9080ea6..5d0f3a8be 100644 --- a/packages/doeff-docker/tests/test_effects.py +++ b/packages/doeff-docker/tests/test_effects.py @@ -7,12 +7,10 @@ from doeff_docker.effects import ( From, Run, Copy, Workdir, SetEnv, Expose, - DockerBuild, DockerRun, ImagePush, + DockerBuild, DockerRun, ) from doeff_docker.handlers.dockerfile import ( collect_dockerfile, - dockerfile_collector_handler, - render_dockerfile, ) From 9f38c74c399bb8ac4c8f49bfaea4075ea843f233 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:57 +0900 Subject: [PATCH 07/35] Clean Ruff violations in doeff-events --- packages/doeff-events/src/doeff_events/handlers/memory.py | 4 ++-- packages/doeff-events/tests/test_multiple_listeners.py | 8 ++++---- packages/doeff-events/tests/test_publish.py | 8 ++++---- packages/doeff-events/tests/test_typed_dispatch.py | 8 ++++---- packages/doeff-events/tests/test_wait_for_event.py | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/doeff-events/src/doeff_events/handlers/memory.py b/packages/doeff-events/src/doeff_events/handlers/memory.py index e6d68f4bc..2ab63a285 100644 --- a/packages/doeff-events/src/doeff_events/handlers/memory.py +++ b/packages/doeff-events/src/doeff_events/handlers/memory.py @@ -1,9 +1,9 @@ """In-memory publish/subscribe handler.""" -from typing import Any + +from doeff_core_effects.scheduler import CompletePromise, CreatePromise, Wait from doeff import Pass, Resume, do -from doeff_core_effects.scheduler import CreatePromise, CompletePromise, Wait from doeff_events.effects import PublishEffect, WaitForEventEffect diff --git a/packages/doeff-events/tests/test_multiple_listeners.py b/packages/doeff-events/tests/test_multiple_listeners.py index 92a64c698..09e976481 100644 --- a/packages/doeff-events/tests/test_multiple_listeners.py +++ b/packages/doeff-events/tests/test_multiple_listeners.py @@ -2,12 +2,12 @@ import asyncio from dataclasses import dataclass +from doeff_core_effects import Await +from doeff_core_effects.scheduler import Spawn, Wait from doeff_events.effects import Publish, WaitForEvent from doeff_events.handlers import event_handler from doeff import WithHandler, do, run -from doeff_core_effects.scheduler import Spawn, Wait, scheduled -from doeff_core_effects import Await @dataclass(frozen=True) @@ -40,12 +40,12 @@ def run_listener(name: str): first_result = run( WithHandler(event_handler(), run_listener("a")), - handlers=default_handlers(), + handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) second_result = run( WithHandler(event_handler(), run_listener("b")), - handlers=default_handlers(), + handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) assert first_result.is_ok() diff --git a/packages/doeff-events/tests/test_publish.py b/packages/doeff-events/tests/test_publish.py index 94066ad20..fec55dbf5 100644 --- a/packages/doeff-events/tests/test_publish.py +++ b/packages/doeff-events/tests/test_publish.py @@ -2,12 +2,12 @@ import asyncio from dataclasses import dataclass +from doeff_core_effects import Await +from doeff_core_effects.scheduler import Spawn, Wait from doeff_events.effects import Publish, WaitForEvent from doeff_events.handlers import event_handler from doeff import WithHandler, do, run -from doeff_core_effects.scheduler import Spawn, Wait, scheduled -from doeff_core_effects import Await @dataclass(frozen=True) @@ -29,7 +29,7 @@ def program(): result = run( WithHandler(event_handler(), program()), - handlers=default_handlers(), + handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) assert result.is_ok() @@ -60,7 +60,7 @@ def program(): result = run( WithHandler(event_handler(), program()), - handlers=default_handlers(), + handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) assert result.is_ok() diff --git a/packages/doeff-events/tests/test_typed_dispatch.py b/packages/doeff-events/tests/test_typed_dispatch.py index da42d1b34..cae422cc4 100644 --- a/packages/doeff-events/tests/test_typed_dispatch.py +++ b/packages/doeff-events/tests/test_typed_dispatch.py @@ -2,12 +2,12 @@ import asyncio from dataclasses import dataclass +from doeff_core_effects import Await +from doeff_core_effects.scheduler import Spawn, Wait from doeff_events.effects import Publish, WaitForEvent from doeff_events.handlers import event_handler from doeff import WithHandler, do, run -from doeff_core_effects.scheduler import Spawn, Wait, scheduled -from doeff_core_effects import Await @dataclass(frozen=True) @@ -50,7 +50,7 @@ def program(): result = run( WithHandler(event_handler(), program()), - handlers=default_handlers(), + handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) assert result.is_ok() @@ -78,7 +78,7 @@ def program(): result = run( WithHandler(event_handler(), program()), - handlers=default_handlers(), + handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) assert result.is_ok() diff --git a/packages/doeff-events/tests/test_wait_for_event.py b/packages/doeff-events/tests/test_wait_for_event.py index 2ea6a2898..ae7c21146 100644 --- a/packages/doeff-events/tests/test_wait_for_event.py +++ b/packages/doeff-events/tests/test_wait_for_event.py @@ -2,12 +2,12 @@ import asyncio from dataclasses import dataclass +from doeff_core_effects import Await +from doeff_core_effects.scheduler import Spawn, Wait from doeff_events.effects import Publish, WaitForEvent from doeff_events.handlers import event_handler from doeff import WithHandler, do, run -from doeff_core_effects.scheduler import Spawn, Wait, scheduled -from doeff_core_effects import Await @dataclass(frozen=True) @@ -34,7 +34,7 @@ def program(): result = run( WithHandler(event_handler(), program()), - handlers=default_handlers(), + handlers=default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) assert result.is_ok() From e67454a076b1efa24f91a90bb1a75d5b47b3fc77 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:57 +0900 Subject: [PATCH 08/35] Clean Ruff violations in doeff-flow --- .../doeff-flow/examples/03_error_handling.py | 2 +- .../examples/04_concurrent_workflows.py | 8 ++--- .../examples/05_durable_execution.py | 4 +-- packages/doeff-flow/src/doeff_flow/cli.py | 32 ++++++++----------- packages/doeff-flow/src/doeff_flow/trace.py | 11 +++---- packages/doeff-flow/tests/test_cli.py | 2 +- 6 files changed, 26 insertions(+), 33 deletions(-) diff --git a/packages/doeff-flow/examples/03_error_handling.py b/packages/doeff-flow/examples/03_error_handling.py index ec7398af7..dd67bc60c 100644 --- a/packages/doeff-flow/examples/03_error_handling.py +++ b/packages/doeff-flow/examples/03_error_handling.py @@ -70,7 +70,7 @@ def process_user_data(user_id: int): # Step 1: Fetch user user = yield fetch_user(user_id) - yield slog(step="data", status="got_user", name=user['name']) + yield slog(step="data", status="got_user", name=user["name"]) # Step 2: Fetch orders orders = yield fetch_orders(user_id) diff --git a/packages/doeff-flow/examples/04_concurrent_workflows.py b/packages/doeff-flow/examples/04_concurrent_workflows.py index 2be9f166b..6b1922889 100644 --- a/packages/doeff-flow/examples/04_concurrent_workflows.py +++ b/packages/doeff-flow/examples/04_concurrent_workflows.py @@ -65,7 +65,7 @@ def worker_workflow(worker_id: str, num_tasks: int): Each worker runs independently and has its own trace. """ yield slog(step="worker", worker_id=worker_id, status="starting", num_tasks=num_tasks) - start_time = datetime.now() + start_time = datetime.now() # noqa: DTZ005 - existing local wall-clock behavior is intentionally unchanged completed_tasks = [] @@ -84,7 +84,7 @@ def worker_workflow(worker_id: str, num_tasks: int): completed_tasks.append(result) yield slog(step="worker", worker_id=worker_id, status="completed", task_id=task_id) - elapsed = (datetime.now() - start_time).total_seconds() + elapsed = (datetime.now() - start_time).total_seconds() # noqa: DTZ005 - existing local wall-clock behavior is intentionally unchanged summary = { "worker_id": worker_id, @@ -121,7 +121,7 @@ def run_concurrent_workers(num_workers: int, tasks_per_worker: int): print("=" * 60 + "\n") # Start all workers - start_time = datetime.now() + start_time = datetime.now() # noqa: DTZ005 - existing local wall-clock behavior is intentionally unchanged with ThreadPoolExecutor(max_workers=num_workers) as executor: futures = [] @@ -132,7 +132,7 @@ def run_concurrent_workers(num_workers: int, tasks_per_worker: int): for future in futures: future.result() - elapsed = (datetime.now() - start_time).total_seconds() + elapsed = (datetime.now() - start_time).total_seconds() # noqa: DTZ005 - existing local wall-clock behavior is intentionally unchanged # Summarize results print("\n" + "=" * 60) diff --git a/packages/doeff-flow/examples/05_durable_execution.py b/packages/doeff-flow/examples/05_durable_execution.py index 18b4415ef..04a50bed7 100644 --- a/packages/doeff-flow/examples/05_durable_execution.py +++ b/packages/doeff-flow/examples/05_durable_execution.py @@ -30,6 +30,8 @@ import time from pathlib import Path +from doeff_core_effects.memo_effects import MemoGet, MemoGetEffect, MemoPut, MemoPutEffect +from doeff_core_effects.storage import SQLiteStorage from doeff_flow import run_workflow from doeff import ( @@ -39,8 +41,6 @@ do, slog, ) -from doeff_core_effects.memo_effects import MemoGet, MemoPut, MemoGetEffect, MemoPutEffect -from doeff_core_effects.storage import SQLiteStorage # ============================================================================= # Simulated Expensive Operations diff --git a/packages/doeff-flow/src/doeff_flow/cli.py b/packages/doeff-flow/src/doeff_flow/cli.py index 08dfd5514..0a52373df 100644 --- a/packages/doeff-flow/src/doeff_flow/cli.py +++ b/packages/doeff-flow/src/doeff_flow/cli.py @@ -107,19 +107,18 @@ def _watch_single( while True: if trace_file.exists(): lines = trace_file.read_text().strip().split("\n") - if lines and lines[0]: - if len(lines) > last_line_count: - last_line_count = len(lines) - data = json.loads(lines[-1]) - live.update(_render_trace_panel(data, status_only=status_only)) - - if exit_on_complete and data["status"] in ("completed", "failed"): - console.print( - f"\n[bold]Workflow {workflow_id} finished:[/bold] " - f"[{'green' if data['status'] == 'completed' else 'red'}]" - f"{data['status']}[/]" - ) - return + if lines and lines[0] and len(lines) > last_line_count: + last_line_count = len(lines) + data = json.loads(lines[-1]) + live.update(_render_trace_panel(data, status_only=status_only)) + + if exit_on_complete and data["status"] in ("completed", "failed"): + console.print( + f"\n[bold]Workflow {workflow_id} finished:[/bold] " + f"[{'green' if data['status'] == 'completed' else 'red'}]" + f"{data['status']}[/]" + ) + return else: live.update( Panel( @@ -131,7 +130,7 @@ def _watch_single( time.sleep(poll_interval) -def _render_trace_panel(data: dict, status_only: bool = False) -> Panel: +def _render_trace_panel(data: dict, status_only: bool = False) -> Panel: # noqa: PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged """Render a single workflow trace as a rich Panel with call tree. Args: @@ -182,10 +181,7 @@ def _render_trace_panel(data: dict, status_only: bool = False) -> Panel: # Use different style for deepest frame (current position or error location) if i == len(trace_frames) - 1: - if is_failed: - label = f"[red bold]✗[/] {label}" - else: - label = f"[yellow]→[/] {label}" + label = f"[red bold]✗[/] {label}" if is_failed else f"[yellow]→[/] {label}" current = current.add(label) diff --git a/packages/doeff-flow/src/doeff_flow/trace.py b/packages/doeff-flow/src/doeff_flow/trace.py index 05ea1d078..fe6b56d9d 100644 --- a/packages/doeff-flow/src/doeff_flow/trace.py +++ b/packages/doeff-flow/src/doeff_flow/trace.py @@ -49,10 +49,7 @@ def get_default_trace_dir() -> Path: # Use XDG_STATE_HOME or default xdg_state_home = os.environ.get("XDG_STATE_HOME") - if xdg_state_home: - base = Path(xdg_state_home) - else: - base = Path.home() / ".local" / "state" + base = Path(xdg_state_home) if xdg_state_home else Path.home() / ".local" / "state" return base / "doeff-flow" @@ -201,7 +198,7 @@ def write_terminal_trace( trace_file = trace_dir / workflow_id / "trace.jsonl" trace_file.parent.mkdir(parents=True, exist_ok=True) - now = datetime.now().isoformat() + now = datetime.now().isoformat() # noqa: DTZ005 - existing local wall-clock behavior is intentionally unchanged error = None result_repr = None @@ -273,7 +270,7 @@ def trace_observer( trace_file = trace_dir / workflow_id / "trace.jsonl" trace_file.parent.mkdir(parents=True, exist_ok=True) - started_at = datetime.now().isoformat() + started_at = datetime.now().isoformat() # noqa: DTZ005 - existing local wall-clock behavior is intentionally unchanged def on_step(snapshot: Any) -> None: # For error cases, use the captured effect trace and error location @@ -372,7 +369,7 @@ def on_step(snapshot: Any) -> None: ), trace=frames, started_at=started_at, - updated_at=datetime.now().isoformat(), + updated_at=datetime.now().isoformat(), # noqa: DTZ005 - existing local wall-clock behavior is intentionally unchanged error=error_msg, result=snapshot.result, gather=gather_state, diff --git a/packages/doeff-flow/tests/test_cli.py b/packages/doeff-flow/tests/test_cli.py index 7f1558b30..9bc4a5f73 100644 --- a/packages/doeff-flow/tests/test_cli.py +++ b/packages/doeff-flow/tests/test_cli.py @@ -169,7 +169,7 @@ def test_last_option(self): # Step 17 should NOT be in the output # (we need to be careful - "17" might appear in other places) # Count rows in the table - with rich, data rows have │ separators - data_rows = [l for l in result.output.split("\n") if "running" in l] + data_rows = [line for line in result.output.split("\n") if "running" in line] assert len(data_rows) == 3 From 7becc1353a9cabf94a05b32ea5b90fb62135ace0 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:57 +0900 Subject: [PATCH 09/35] Clean Ruff violations in doeff-gemini --- .../doeff-gemini/src/doeff_gemini/__init__.py | 8 +++--- .../doeff-gemini/src/doeff_gemini/client.py | 6 ++-- .../doeff-gemini/src/doeff_gemini/costs.py | 2 +- .../src/doeff_gemini/effects/__init__.py | 2 +- .../src/doeff_gemini/effects/cost.py | 1 - .../src/doeff_gemini/handlers/production.py | 7 ++--- .../src/doeff_gemini/handlers/testing.py | 2 +- .../src/doeff_gemini/structured_llm.py | 28 ++++++++----------- .../doeff-gemini/src/doeff_gemini/types.py | 2 +- .../tests/e2e/test_gemini_live.py | 2 +- .../doeff-gemini/tests/unit/test_cost_hook.py | 3 +- .../tests/unit/test_effect_handlers.py | 2 +- .../tests/unit/test_structured_llm.py | 2 +- 13 files changed, 30 insertions(+), 37 deletions(-) diff --git a/packages/doeff-gemini/src/doeff_gemini/__init__.py b/packages/doeff-gemini/src/doeff_gemini/__init__.py index a9b1ba56b..228649421 100644 --- a/packages/doeff-gemini/src/doeff_gemini/__init__.py +++ b/packages/doeff-gemini/src/doeff_gemini/__init__.py @@ -50,9 +50,10 @@ __all__ = [ "APICallMetadata", "CostInfo", + "FileUriContentPart", + "GeminiCalculateCost", "GeminiCallResult", "GeminiChat", - "GeminiCalculateCost", "GeminiClient", "GeminiContentPart", "GeminiCostEstimate", @@ -61,7 +62,6 @@ "GeminiImageEditResult", "GeminiStreamingChat", "GeminiStructuredOutput", - "FileUriContentPart", "ImageEdit", "ImageGenerate", "ImageResult", @@ -75,10 +75,10 @@ "calculate_cost", "default_gemini_cost_handler", "edit_image__gemini", - "gemini_mock_handler", - "gemini_production_handler", "gemini_cost_calculator__default", "gemini_image_handler", + "gemini_mock_handler", + "gemini_production_handler", "get_gemini_client", "image_edit__gemini", "mock_handlers", diff --git a/packages/doeff-gemini/src/doeff_gemini/client.py b/packages/doeff-gemini/src/doeff_gemini/client.py index f70d1a3f3..91b9dfa39 100644 --- a/packages/doeff-gemini/src/doeff_gemini/client.py +++ b/packages/doeff-gemini/src/doeff_gemini/client.py @@ -62,7 +62,7 @@ def _prepare_request_payload( return sanitized_payload, text, serialized_images -def _summarize_payload_for_log(value: Any, *, max_string_length: int = 200) -> Any: +def _summarize_payload_for_log(value: Any, *, max_string_length: int = 200) -> Any: # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged """Produce a repr-friendly copy of payload data with bounded string lengths.""" if isinstance(value, str): @@ -205,7 +205,7 @@ def _result_err(exc: Exception) -> EffectGenerator[Any]: @do -def get_gemini_client() -> EffectGenerator[GeminiClient]: +def get_gemini_client() -> EffectGenerator[GeminiClient]: # noqa: PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged """Retrieve a configured :class:`GeminiClient` from Reader or State effects.""" @do @@ -385,7 +385,7 @@ def _get_attr(*names: str) -> int | None: @do -def track_api_call( +def track_api_call( # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged operation: str, model: str, request_summary: dict[str, Any], diff --git a/packages/doeff-gemini/src/doeff_gemini/costs.py b/packages/doeff-gemini/src/doeff_gemini/costs.py index 746fbe74d..c0d66091a 100644 --- a/packages/doeff-gemini/src/doeff_gemini/costs.py +++ b/packages/doeff-gemini/src/doeff_gemini/costs.py @@ -218,6 +218,6 @@ def gemini_cost_calculator__default( "UnknownModelPricingError", "calculate_cost", "calculate_known_model_cost", - "get_model_pricing", "gemini_cost_calculator__default", + "get_model_pricing", ] diff --git a/packages/doeff-gemini/src/doeff_gemini/effects/__init__.py b/packages/doeff-gemini/src/doeff_gemini/effects/__init__.py index f214c5eee..370733df5 100644 --- a/packages/doeff-gemini/src/doeff_gemini/effects/__init__.py +++ b/packages/doeff-gemini/src/doeff_gemini/effects/__init__.py @@ -10,8 +10,8 @@ from .structured import GeminiStructuredOutput __all__ = [ - "GeminiChat", "GeminiCalculateCost", + "GeminiChat", "GeminiEmbedding", "GeminiImageEdit", "GeminiStreamingChat", diff --git a/packages/doeff-gemini/src/doeff_gemini/effects/cost.py b/packages/doeff-gemini/src/doeff_gemini/effects/cost.py index db6f2aad4..f89e9cebc 100644 --- a/packages/doeff-gemini/src/doeff_gemini/effects/cost.py +++ b/packages/doeff-gemini/src/doeff_gemini/effects/cost.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from doeff import EffectBase - from doeff_gemini.types import GeminiCallResult diff --git a/packages/doeff-gemini/src/doeff_gemini/handlers/production.py b/packages/doeff-gemini/src/doeff_gemini/handlers/production.py index aa1314889..5b7f32303 100644 --- a/packages/doeff-gemini/src/doeff_gemini/handlers/production.py +++ b/packages/doeff-gemini/src/doeff_gemini/handlers/production.py @@ -4,10 +4,11 @@ import json import math import time -from collections.abc import Callable +from collections.abc import Callable, Generator from io import BytesIO from typing import Any, cast +from doeff_core_effects import Await, Try from doeff_image.effects import ImageEdit, ImageGenerate from doeff_image.types import ImageResult from doeff_llm.effects import ( @@ -19,8 +20,6 @@ from PIL import Image from doeff import EffectBase, Pass, Resume, do -from doeff_core_effects import Await, Try -from typing import Generator 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 ( @@ -429,7 +428,7 @@ def production_handlers( active_cost_handler = cost_handler @do - def handler(effect: Any, k: Any): + def handler(effect: Any, k: Any): # noqa: PLR0911, PLR0912 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, GeminiCalculateCost): if active_cost_handler is None: yield Pass(effect, k) diff --git a/packages/doeff-gemini/src/doeff_gemini/handlers/testing.py b/packages/doeff-gemini/src/doeff_gemini/handlers/testing.py index 722fe51fe..d34d37431 100644 --- a/packages/doeff-gemini/src/doeff_gemini/handlers/testing.py +++ b/packages/doeff-gemini/src/doeff_gemini/handlers/testing.py @@ -188,7 +188,7 @@ def mock_handlers( ) @do - def handler(effect: Any, k: Any): + def handler(effect: Any, k: Any): # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, LLMStreamingChat | GeminiStreamingChat): if not _is_gemini_model(effect.model): yield Pass(effect, k) diff --git a/packages/doeff-gemini/src/doeff_gemini/structured_llm.py b/packages/doeff-gemini/src/doeff_gemini/structured_llm.py index ee7ea740f..7b8ef79f0 100644 --- a/packages/doeff-gemini/src/doeff_gemini/structured_llm.py +++ b/packages/doeff-gemini/src/doeff_gemini/structured_llm.py @@ -23,6 +23,9 @@ except ImportError: # pragma: no cover - Python 3.10 fallback from typing_extensions import NotRequired, TypedDict +from doeff_core_effects.memo_effects import MemoGet, MemoPut +from doeff_core_effects.memo_policy import Lifecycle, RecomputeCost + from doeff import ( Ask, Await, @@ -32,8 +35,6 @@ do, slog, ) -from doeff_core_effects.memo_effects import MemoGet, MemoPut -from doeff_core_effects.memo_policy import Lifecycle, RecomputeCost from .client import get_gemini_client, track_api_call from .types import GeminiImageEditResult @@ -516,7 +517,7 @@ def build_generation_config( @do -def process_structured_response( +def process_structured_response( # noqa: PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged response: Any, response_format: type[BaseModel], ) -> EffectGenerator[Any]: @@ -555,9 +556,8 @@ def process_structured_response( if stripped: try: payload = json.loads(stripped) - except json.JSONDecodeError: + except json.JSONDecodeError as exc: preview = _stringify_for_log(stripped, limit=200) - raw_content_for_error = stripped yield Tell("Gemini json payload could not be decoded") raise GeminiStructuredOutputError( format_name=format_name, @@ -565,7 +565,7 @@ def process_structured_response( message=( f"Gemini returned invalid structured payload for {format_name}: {preview}" ), - ) + ) from exc else: payload = None @@ -574,11 +574,10 @@ def process_structured_response( preview = _stringify_for_log(raw_text, limit=200) yield Tell(f"Parsing Gemini structured response fall back to text: {preview}") stripped = raw_text.strip() - raw_content_for_error = raw_text if stripped and stripped[0] in "[{": try: payload = json.loads(stripped) - except json.JSONDecodeError: + except json.JSONDecodeError as exc: yield Tell("Gemini response text was not valid JSON") yield Tell(f"Raw content: {preview}") raise GeminiStructuredOutputError( @@ -587,7 +586,7 @@ def process_structured_response( message=( f"Gemini returned non-JSON structured output for {format_name}: {preview}" ), - ) + ) from exc else: yield Tell("Gemini response did not include JSON payload") raise GeminiStructuredOutputError( @@ -817,10 +816,7 @@ def repair_structured_response( elif isinstance(fixed_value, Mapping): payload = dict(fixed_value) else: - if not isinstance(fixed_value, str): - fixed_text = str(fixed_value) - else: - fixed_text = fixed_value + fixed_text = str(fixed_value) if not isinstance(fixed_value, str) else fixed_value try: payload = json.loads(fixed_text) except json.JSONDecodeError as exc: # pragma: no cover - defensive logging @@ -846,7 +842,7 @@ def repair_structured_response( @do -def process_image_edit_response(response: Any) -> EffectGenerator[GeminiImageEditResult]: +def process_image_edit_response(response: Any) -> EffectGenerator[GeminiImageEditResult]: # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged """Extract image bytes and optional text from a Gemini response.""" image_bytes: bytes | None = None @@ -1044,7 +1040,7 @@ def api_call_with_tracking() -> EffectGenerator[Any]: safe_retry_result = yield Try( _retry_with_backoff( - lambda: make_api_call(), + make_api_call, max_attempts=max_retries, delay_strategy=_gemini_random_backoff, ) @@ -1249,7 +1245,7 @@ def api_call_with_tracking() -> EffectGenerator[Any]: safe_retry_result = yield Try( _retry_with_backoff( - lambda: make_api_call(), + make_api_call, max_attempts=max_retries, delay_strategy=_gemini_random_backoff, ) diff --git a/packages/doeff-gemini/src/doeff_gemini/types.py b/packages/doeff-gemini/src/doeff_gemini/types.py index 1776cc06a..f9d99fdfd 100644 --- a/packages/doeff-gemini/src/doeff_gemini/types.py +++ b/packages/doeff-gemini/src/doeff_gemini/types.py @@ -26,7 +26,7 @@ def to_pil_image(self): # type: ignore[override] image = Image.open(buffer) return image.copy() - def save(self, path: str, *, format: str | None = None) -> None: + def save(self, path: str, *, format: str | None = None) -> None: # noqa: A002 - public API parameter name is intentionally stable """Persist the edited image to disk.""" image = self.to_pil_image() diff --git a/packages/doeff-gemini/tests/e2e/test_gemini_live.py b/packages/doeff-gemini/tests/e2e/test_gemini_live.py index 8a43da617..4b4668c20 100644 --- a/packages/doeff-gemini/tests/e2e/test_gemini_live.py +++ b/packages/doeff-gemini/tests/e2e/test_gemini_live.py @@ -21,6 +21,7 @@ if str(PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(PACKAGE_ROOT)) +from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 from doeff_gemini import edit_image__gemini, structured_llm__gemini # noqa: E402 from doeff_gemini.handlers import default_gemini_cost_handler # noqa: E402 @@ -34,7 +35,6 @@ default_handlers, do, ) -from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 class FunFact(BaseModel): diff --git a/packages/doeff-gemini/tests/unit/test_cost_hook.py b/packages/doeff-gemini/tests/unit/test_cost_hook.py index 1f9a2c306..421a63db2 100644 --- a/packages/doeff-gemini/tests/unit/test_cost_hook.py +++ b/packages/doeff-gemini/tests/unit/test_cost_hook.py @@ -68,8 +68,7 @@ class ValidationError(Exception): # type: ignore[no-redef] from doeff_gemini.effects import GeminiCalculateCost from doeff_gemini.handlers import default_gemini_cost_handler -from doeff import Pass, Resume, async_run, default_handlers, do -from doeff_core_effects.effects import EffectBase +from doeff import Effect, Pass, Resume, async_run, default_handlers, do def _fake_response(usage: dict[str, int]) -> Any: diff --git a/packages/doeff-gemini/tests/unit/test_effect_handlers.py b/packages/doeff-gemini/tests/unit/test_effect_handlers.py index f451d140b..1b093d542 100644 --- a/packages/doeff-gemini/tests/unit/test_effect_handlers.py +++ b/packages/doeff-gemini/tests/unit/test_effect_handlers.py @@ -58,6 +58,7 @@ def model_validate(cls, value: Any) -> Any: from pydantic import BaseModel from doeff import ( + Effect, Delegate, EffectGenerator, Resume, @@ -66,7 +67,6 @@ def model_validate(cls, value: Any) -> Any: do, run, ) -from doeff_core_effects.effects import EffectBase from doeff_image.effects import ImageEdit from doeff_image.effects import ImageEdit as UnifiedImageEdit from doeff_image.effects import ImageGenerate as UnifiedImageGenerate diff --git a/packages/doeff-gemini/tests/unit/test_structured_llm.py b/packages/doeff-gemini/tests/unit/test_structured_llm.py index 6c6f31c76..539b29918 100644 --- a/packages/doeff-gemini/tests/unit/test_structured_llm.py +++ b/packages/doeff-gemini/tests/unit/test_structured_llm.py @@ -45,10 +45,10 @@ from doeff_gemini.types import APICallMetadata genai_types = google_genai.types +from doeff_core_effects.memo_effects import MemoGetEffect, MemoPutEffect from pydantic import BaseModel from doeff import EffectGenerator, async_run, default_handlers, do -from doeff_core_effects.memo_effects import MemoGetEffect, MemoPut, MemoPutEffect structured_llm_module = importlib.import_module("doeff_gemini.structured_llm") From 02739b0e83580c632b26c4fcfae4ef6ee2a33ba7 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 10/35] Clean Ruff violations in doeff-git --- packages/doeff-git/src/doeff_git/handlers/production.py | 2 +- packages/doeff-git/src/doeff_git/handlers/testing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/doeff-git/src/doeff_git/handlers/production.py b/packages/doeff-git/src/doeff_git/handlers/production.py index f2f53f190..badb05b6a 100644 --- a/packages/doeff-git/src/doeff_git/handlers/production.py +++ b/packages/doeff-git/src/doeff_git/handlers/production.py @@ -185,7 +185,7 @@ def production_handlers( hosting = github_handler or GitHubHandler() @do - def handler(effect: Effect, k: Any): + def handler(effect: Effect, k: Any): # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, GitCommit): return (yield Resume(k, local.handle_commit(effect))) if isinstance(effect, GitDiff): diff --git a/packages/doeff-git/src/doeff_git/handlers/testing.py b/packages/doeff-git/src/doeff_git/handlers/testing.py index 28905ef01..e374454b6 100644 --- a/packages/doeff-git/src/doeff_git/handlers/testing.py +++ b/packages/doeff-git/src/doeff_git/handlers/testing.py @@ -93,7 +93,7 @@ def mock_handlers( active_runtime = runtime or MockGitRuntime() @do - def handler(effect: Effect, k: Any): + def handler(effect: Effect, k: Any): # noqa: PLR0911 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, GitCommit): return (yield Resume(k, active_runtime.handle_commit(effect))) if isinstance(effect, GitDiff): From f1ee31246bca7f13b8c2b3d850768933ed40bbb2 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 11/35] Clean Ruff violations in doeff-google-secret-manager --- .../src/doeff_google_secret_manager/client.py | 2 +- .../handlers/production.py | 1 - .../doeff_google_secret_manager/handlers/testing.py | 1 - .../tests/unit/test_secrets.py | 11 +++++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/client.py b/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/client.py index d24f9fba8..f5c9c7410 100644 --- a/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/client.py +++ b/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/client.py @@ -63,7 +63,7 @@ def async_client(self): @do -def get_secret_manager_client() -> EffectGenerator[SecretManagerClient]: +def get_secret_manager_client() -> EffectGenerator[SecretManagerClient]: # noqa: PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged """Retrieve or construct a :class:`SecretManagerClient` using ADC when available.""" @do 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 9183cd5fb..803bc566e 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,7 +9,6 @@ from doeff_secret.effects import DeleteSecret, GetSecret, ListSecrets, SetSecret from doeff import Effect, Pass, Resume, do -from doeff_core_effects.effects import EffectBase from doeff_google_secret_manager.client import SecretManagerClient, get_secret_manager_client ProtocolHandler = Callable[[Any, Any], Any] diff --git a/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/handlers/testing.py b/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/handlers/testing.py index 73ea419f4..b81c2d9ef 100644 --- a/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/handlers/testing.py +++ b/packages/doeff-google-secret-manager/src/doeff_google_secret_manager/handlers/testing.py @@ -3,7 +3,6 @@ import warnings from collections.abc import Mapping -from typing import Any from doeff_secret.testing import ( InMemorySecretStore, 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 9d6b7183a..429393bed 100644 --- a/packages/doeff-google-secret-manager/tests/unit/test_secrets.py +++ b/packages/doeff-google-secret-manager/tests/unit/test_secrets.py @@ -14,13 +14,14 @@ if str(SECRET_PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(SECRET_PACKAGE_ROOT)) +from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 from doeff_google_secret_manager import ( # noqa: E402 SecretManagerClient, access_secret, get_secret_manager_client, ) -from doeff import ( # noqa: E402 +from doeff import ( # noqa: E402 - late import preserves existing import/setup order # noqa: E402 AskEffect, Pass, Resume, @@ -29,8 +30,10 @@ do, run, ) -from doeff import Get as StateGetEffect, Put as StatePutEffect # noqa: E402 -from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 +from doeff import Get as StateGetEffect # noqa: E402 +from doeff import ( # noqa: E402 - late import preserves existing import/setup order + Put as StatePutEffect, +) @dataclass @@ -52,7 +55,7 @@ async def access_secret_version(self, request: dict[str, Any]) -> Any: return SimpleNamespace(payload=SimpleNamespace(data=self.payload)) -class FakeNotFound(Exception): +class FakeNotFound(Exception): # noqa: N818 - public or fixture exception name is intentionally stable """Fake NotFound exception for error propagation tests.""" From 122a2668dc4a6082a2b4a673d2cd480d7a87c877 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 12/35] Clean Ruff violations in doeff-hy --- packages/doeff-hy/src/doeff_hy/__init__.py | 2 +- packages/doeff-hy/src/doeff_hy/http.pyi | 1 - .../doeff-hy/tests/multimod/python_layer.py | 3 +- .../doeff-hy/tests/test_lazy_and_threading.py | 79 ++++++++----------- .../doeff-hy/tests/test_tail_resume_tco.py | 8 +- 5 files changed, 40 insertions(+), 53 deletions(-) diff --git a/packages/doeff-hy/src/doeff_hy/__init__.py b/packages/doeff-hy/src/doeff_hy/__init__.py index 03b2af821..2740ba347 100644 --- a/packages/doeff-hy/src/doeff_hy/__init__.py +++ b/packages/doeff-hy/src/doeff_hy/__init__.py @@ -12,7 +12,7 @@ import importlib.machinery import os -import hy.importer # noqa: F401 — activates Hy import hooks +import hy.importer # Register .hyk and .hyp as Hy source extensions for _ext in (".hyk", ".hyp"): diff --git a/packages/doeff-hy/src/doeff_hy/http.pyi b/packages/doeff-hy/src/doeff_hy/http.pyi index a51c7ee64..d0d1808e8 100644 --- a/packages/doeff-hy/src/doeff_hy/http.pyi +++ b/packages/doeff-hy/src/doeff_hy/http.pyi @@ -1,6 +1,5 @@ from typing import Any - def http_get( url: str, *, diff --git a/packages/doeff-hy/tests/multimod/python_layer.py b/packages/doeff-hy/tests/multimod/python_layer.py index da6c6602b..b1304e85a 100644 --- a/packages/doeff-hy/tests/multimod/python_layer.py +++ b/packages/doeff-hy/tests/multimod/python_layer.py @@ -1,6 +1,7 @@ """Python @do functions — no __doeff_body__.""" from dataclasses import dataclass -from doeff import do, EffectBase + +from doeff import EffectBase, do @dataclass(frozen=True) diff --git a/packages/doeff-hy/tests/test_lazy_and_threading.py b/packages/doeff-hy/tests/test_lazy_and_threading.py index 82cf95c51..8649cc8a5 100644 --- a/packages/doeff-hy/tests/test_lazy_and_threading.py +++ b/packages/doeff-hy/tests/test_lazy_and_threading.py @@ -5,14 +5,15 @@ 2. lazy clause in defk — same mechanism 3. <-> effectful threading macro """ -import pytest +from dataclasses import dataclass -from doeff import do, run as doeff_run, WithHandler, EffectBase, Some, Nothing -from doeff_core_effects.effects import Ask, Get, Put -from doeff_core_effects.handlers import state, lazy_ask +import pytest +from doeff_core_effects.effects import Get, Put +from doeff_core_effects.handlers import lazy_ask, state from doeff_core_effects.scheduler import scheduled -from dataclasses import dataclass +from doeff import EffectBase, Some, WithHandler, do +from doeff import run as doeff_run # ── Test effects ────────────────────────────────────────────────── @@ -51,8 +52,8 @@ class TestLazyHandlerMacro: def test_lazy_handler_basic(self): """Lazy init runs on first effect, uses cached value on second.""" - import hy # noqa: F401 import doeff_hy # noqa: F401 + import hy # This Hy code uses the lazy clause in defhandler code = """ @@ -94,14 +95,14 @@ def test_lazy_handler_basic(self): def test_lazy_handler_runs_once(self): """Lazy init should execute only once even with multiple effect invocations.""" - import hy # noqa: F401 import doeff_hy # noqa: F401 + import hy # noqa: F401 # We test by checking state key contains Some after first init @do def handler_with_lazy(effect, k): """Simulates what the lazy macro should expand to.""" - from doeff.program import Resume, Pass + from doeff.program import Pass, Resume if isinstance(effect, FetchData): # --- This is what lazy macro should generate --- cached = yield Get(f"{__name__}/handler_with_lazy/client") @@ -133,7 +134,7 @@ def test_lazy_handler_none_value_via_some(self): @do def handler_with_lazy_none(effect, k): - from doeff.program import Resume, Pass + from doeff.program import Pass, Resume if isinstance(effect, FetchData): cached = yield Get(f"{__name__}/handler/val") if isinstance(cached, Some): @@ -168,8 +169,8 @@ class TestLazyHandlerHyIntegration: def test_defhandler_lazy_compiles(self): """(defhandler name (lazy x ...) (Effect [f] ...)) should compile.""" - import hy import doeff_hy # noqa: F401 + import hy # This should NOT raise a SyntaxError once implemented code = """ @@ -187,8 +188,8 @@ def test_defhandler_lazy_compiles(self): def test_defhandler_lazy_with_effects(self): """Lazy init body can perform effects (Ask) for config.""" - import hy import doeff_hy # noqa: F401 + import hy code = """ (require doeff-hy.macros [defp <-]) @@ -234,16 +235,17 @@ def test_defhandler_lazy_with_effects(self): def test_defhandler_lazy_key_contains_module(self): """Lazy state key should be prefixed with module name.""" - import hy - import doeff_hy # noqa: F401 import types + import doeff_hy # noqa: F401 + import hy + observed_keys = [] # Create a spy state handler that records Get keys @do def spy_state(effect, k): - from doeff.program import Resume, Pass + from doeff.program import Pass, Resume if isinstance(effect, Get): observed_keys.append(effect.key) result = yield Resume(k, None) @@ -303,10 +305,11 @@ class TestLazyDefk: def test_defk_lazy_basic(self): """(defk name [x] (lazy val ...) body) should compile and run.""" - import hy - import doeff_hy # noqa: F401 import types + import doeff_hy # noqa: F401 + import hy + code = """ (require doeff-hy.macros [defk defp <-]) (import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) @@ -352,10 +355,11 @@ class TestThreadingMacro: def test_threading_expands_correctly(self): """(<-> (f) (g :k v) (h)) should thread first-arg through pipeline.""" - import hy - import doeff_hy # noqa: F401 import types + import doeff_hy # noqa: F401 + import hy + code = """ (require doeff-hy.macros [defk defp <- <->]) (import doeff [do :as _doeff-do EffectBase WithHandler run :as doeff-run]) @@ -401,10 +405,11 @@ def test_threading_expands_correctly(self): def test_threading_single_form(self): """(<-> (f x)) with one form is just (<- _t0 (f x)), _t0.""" - import hy - import doeff_hy # noqa: F401 import types + import doeff_hy # noqa: F401 + import hy + code = """ (require doeff-hy.macros [defk defp <- <->]) (import doeff [do :as _doeff-do EffectBase run :as doeff-run]) @@ -432,10 +437,11 @@ def test_threading_single_form(self): def test_threading_two_forms(self): """(<-> (f) (g)) threads result of f as first arg to g.""" - import hy - import doeff_hy # noqa: F401 import types + import doeff_hy # noqa: F401 + import hy + code = """ (require doeff-hy.macros [defk defp <- <->]) (import doeff [do :as _doeff-do EffectBase run :as doeff-run]) @@ -473,10 +479,11 @@ def test_threading_two_forms(self): def _hy_eval_in_module(code, mod_name): """Helper: eval Hy code in a fresh module, return the module.""" - import hy - import doeff_hy # noqa: F401 - import types import sys + import types + + import doeff_hy # noqa: F401 + import hy mod = types.ModuleType(mod_name) mod.__file__ = "" @@ -491,24 +498,6 @@ class TestLazyValVar: def test_lazy_val_is_alias_for_lazy(self): """(lazy-val name body) should work identically to (lazy name body).""" import sys - 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_core_effects [Ask Get Put state]) -(import doeff_core_effects.handlers [lazy-ask]) -(import doeff_core_effects.scheduler [scheduled]) -(import dataclasses [dataclass]) - -(defclass [(dataclass :frozen True)] Ping [EffectBase]) - -(defhandler ping-handler - (lazy-val greeting "hello") - (Ping [] (resume greeting))) - -(setv wrapped (WithHandler ping-handler - (WithHandler (state) body))) -""" # Build body separately to avoid defp issues code2 = """ (require doeff-hy.macros [defp <-]) @@ -623,8 +612,8 @@ def test_lazy_var_set_bang_persists_across_calls(self): def test_lazy_val_set_bang_compile_error(self): """set! on lazy-val should raise SyntaxError at compile time.""" - import hy import doeff_hy # noqa: F401 + import hy code = """ (require doeff-hy.macros [set!]) @@ -640,7 +629,7 @@ def test_lazy_val_set_bang_compile_error(self): (set! x 1) (resume x))) """ - with pytest.raises(Exception, match="lazy-val.*immutable|set!.*lazy-val"): + with pytest.raises(Exception, match=r"lazy-val.*immutable|set!.*lazy-val"): hy.eval(hy.read_many(code)) def test_lazy_var_in_defk(self): diff --git a/packages/doeff-hy/tests/test_tail_resume_tco.py b/packages/doeff-hy/tests/test_tail_resume_tco.py index 77f505f53..74bfa4dd4 100644 --- a/packages/doeff-hy/tests/test_tail_resume_tco.py +++ b/packages/doeff-hy/tests/test_tail_resume_tco.py @@ -7,14 +7,12 @@ 1. Macro-level: _build_clause output contains Transfer, not Resume 2. Integration: handler with many Resume-loop effects doesn't accumulate frames """ -import pytest -import hy # noqa: F401 import doeff_hy # noqa: F401 — registers macros/extensions -from hy.models import Expression, Symbol, List as HyList, Integer as HyInt - +import hy # noqa: F401 from doeff_hy.handle import _build_clause - +from hy.models import Expression, Symbol +from hy.models import List as HyList # --------------------------------------------------------------------------- # Helpers From 8bfac2ab9f10a45b58330c323e1601d872e1b24e Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 13/35] Clean Ruff violations in doeff-image --- packages/doeff-image/src/doeff_image/types.py | 2 +- packages/doeff-image/tests/test_multi_provider_workflow.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/doeff-image/src/doeff_image/types.py b/packages/doeff-image/src/doeff_image/types.py index 018ed249b..ce7801296 100644 --- a/packages/doeff-image/src/doeff_image/types.py +++ b/packages/doeff-image/src/doeff_image/types.py @@ -24,7 +24,7 @@ def to_pil_image(self): # type: ignore[override] raise ValueError("ImageResult has no images") return self.images[0] - def save(self, path: str, *, format: str | None = None) -> None: + def save(self, path: str, *, format: str | None = None) -> None: # noqa: A002 - public API parameter name is intentionally stable """Save the first image in the result.""" self.to_pil_image().save(path, format=format) diff --git a/packages/doeff-image/tests/test_multi_provider_workflow.py b/packages/doeff-image/tests/test_multi_provider_workflow.py index 3ab44462a..e3745e9c0 100644 --- a/packages/doeff-image/tests/test_multi_provider_workflow.py +++ b/packages/doeff-image/tests/test_multi_provider_workflow.py @@ -43,12 +43,12 @@ class ValidationError(Exception): # type: ignore[no-redef] pydantic_stub.ValidationError = ValidationError # type: ignore[attr-defined] sys.modules["pydantic"] = pydantic_stub -from doeff import Delegate, EffectGenerator, Resume, WithHandler, default_handlers, do, run +import doeff_gemini.handlers.production as gemini_production +import doeff_seedream.handlers.production as seedream_production from doeff_image.effects import ImageEdit, ImageGenerate from doeff_image.types import ImageResult -import doeff_gemini.handlers.production as gemini_production -import doeff_seedream.handlers.production as seedream_production +from doeff import Delegate, EffectGenerator, Resume, WithHandler, default_handlers, do, run def _fallback_handler(value: str): From 45c5becc0237259184f28ace1299fd233f5fdfc2 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 14/35] Clean Ruff violations in doeff-indexer --- .../tests/fixtures/test_comprehensive_detection.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/doeff-indexer/tests/fixtures/test_comprehensive_detection.py b/packages/doeff-indexer/tests/fixtures/test_comprehensive_detection.py index 702d61dc5..86be10325 100644 --- a/packages/doeff-indexer/tests/fixtures/test_comprehensive_detection.py +++ b/packages/doeff-indexer/tests/fixtures/test_comprehensive_detection.py @@ -3,10 +3,12 @@ Each section tests specific detection rules. """ +from __future__ import annotations + from collections.abc import Iterable from typing import Any, Generic, TypeVar -from doeff import Effect, Program, do +from doeff import Effect, Program, Tell, do # =========================================================================== # SECTION 1: INTERPRETERS (Program[T] -> Any where Any != Program) @@ -195,7 +197,7 @@ def fetch_data(self, key: str) -> Data: # =========================================================================== class User: - def __init__(self, id: str): self.id = id + def __init__(self, id: str): self.id = id # noqa: A002 - public API parameter name is intentionally stable class Result: def __init__(self, data: Any): self.data = data @@ -206,7 +208,7 @@ def __init__(self, key: str): self.key = key class LogEffect(Effect): def __init__(self, message: str): self.message = message -def Log(msg: str): return LogEffect(msg) +def Log(msg: str): return LogEffect(msg) # noqa: N802 - public or spec test name is intentionally stable T = TypeVar("T") From 7fe979c7cda380f941030e6d3296a04032aaa634 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 15/35] Clean Ruff violations in doeff-linter --- packages/doeff-linter/tests/fixtures/test_noqa.py | 4 ++-- packages/doeff-linter/tests/fixtures/test_sample.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/doeff-linter/tests/fixtures/test_noqa.py b/packages/doeff-linter/tests/fixtures/test_noqa.py index 8ef2fbf6e..1321fe6d7 100644 --- a/packages/doeff-linter/tests/fixtures/test_noqa.py +++ b/packages/doeff-linter/tests/fixtures/test_noqa.py @@ -4,7 +4,7 @@ # This should NOT trigger because of noqa -def dict(): +def dict(): # noqa: A001 - fixture intentionally uses builtin shadowing sample return {} @@ -14,7 +14,7 @@ def list(): # noqa # This should still trigger DOEFF009 (no return type) -def set(): +def set(): # noqa: A001 - fixture intentionally uses builtin shadowing sample return set() diff --git a/packages/doeff-linter/tests/fixtures/test_sample.py b/packages/doeff-linter/tests/fixtures/test_sample.py index 553248627..7acd39c41 100644 --- a/packages/doeff-linter/tests/fixtures/test_sample.py +++ b/packages/doeff-linter/tests/fixtures/test_sample.py @@ -5,7 +5,7 @@ # DOEFF001: Builtin shadowing -def dict(): # Should trigger DOEFF001 +def dict(): # Should trigger DOEFF001 # noqa: A001 - fixture intentionally uses builtin shadowing sample return {} From aedd05c7b1bc99e2d70ad2d4cf24ec1bc02b31cd Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 16/35] Clean Ruff violations in doeff-llm --- packages/doeff-llm/src/doeff_llm/effects/embedding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/doeff-llm/src/doeff_llm/effects/embedding.py b/packages/doeff-llm/src/doeff_llm/effects/embedding.py index 63c952646..1a2e84dcf 100644 --- a/packages/doeff-llm/src/doeff_llm/effects/embedding.py +++ b/packages/doeff-llm/src/doeff_llm/effects/embedding.py @@ -6,7 +6,7 @@ class LLMEmbedding(EffectBase): """Request provider-agnostic embeddings.""" - def __init__(self, *, input: str | list[str], model: str): + def __init__(self, *, input: str | list[str], model: str): # noqa: A002 - public API parameter name is intentionally stable super().__init__() self.input = input self.model = model From d8224d947feba9f619eae8972667b55f44c9bd6e Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 17/35] Clean Ruff violations in doeff-ml-nexus --- packages/doeff-ml-nexus/tests/test_docker.py | 7 +++---- packages/doeff-ml-nexus/tests/test_uv_deps.py | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/doeff-ml-nexus/tests/test_docker.py b/packages/doeff-ml-nexus/tests/test_docker.py index b5009bbb8..c6bb2f6e7 100644 --- a/packages/doeff-ml-nexus/tests/test_docker.py +++ b/packages/doeff-ml-nexus/tests/test_docker.py @@ -1,7 +1,6 @@ """Tests for uv-image Dockerfile generation.""" import hy # noqa: F401 -from pathlib import Path from doeff import run, WithHandler, do from doeff_core_effects import reader, writer, slog_handler @@ -28,9 +27,9 @@ def test(): result = _run_with_handlers(test()) lines = result.split("\n") assert lines[0] == "FROM python:3.13-slim" - assert any("uv" in l for l in lines) - assert any("uv sync --frozen --no-install-project" in l for l in lines) - assert any("uv sync --frozen" in l for l in lines) + assert any("uv" in line for line in lines) + assert any("uv sync --frozen --no-install-project" in line for line in lines) + assert any("uv sync --frozen" in line for line in lines) def test_gpu_image_has_nvidia_env(self): @do diff --git a/packages/doeff-ml-nexus/tests/test_uv_deps.py b/packages/doeff-ml-nexus/tests/test_uv_deps.py index aab91038e..153b72438 100644 --- a/packages/doeff-ml-nexus/tests/test_uv_deps.py +++ b/packages/doeff-ml-nexus/tests/test_uv_deps.py @@ -1,7 +1,6 @@ """Tests for uv dependency detection and rewriting.""" import hy # noqa: F401 -import tempfile from pathlib import Path from doeff_ml_nexus.uv_deps import ( From 07ad64784d2e739338423359837038bfd3647f9e Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 18/35] Clean Ruff violations in doeff-notify --- packages/doeff-notify/tests/test_handlers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/doeff-notify/tests/test_handlers.py b/packages/doeff-notify/tests/test_handlers.py index 81e73e5ab..0b0b6e175 100644 --- a/packages/doeff-notify/tests/test_handlers.py +++ b/packages/doeff-notify/tests/test_handlers.py @@ -22,8 +22,7 @@ ) from doeff_notify.types import Channel, NotificationResult, Urgency -from doeff import Effect, Pass, Resume, WithHandler, default_handlers, do, run -from doeff import WriterTellEffect +from doeff import Effect, Pass, Resume, WithHandler, WriterTellEffect, default_handlers, do, run def _is_ok(run_result: Any) -> bool: From e03d0adf3e8d3465d4a8835ebcf3146cbf0f7e25 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 19/35] Clean Ruff violations in doeff-openai --- .../examples/structured_llm_example.py | 6 +-- .../doeff-openai/src/doeff_openai/__init__.py | 10 ++-- .../doeff-openai/src/doeff_openai/chat.py | 21 ++++---- .../doeff-openai/src/doeff_openai/client.py | 11 ++-- .../doeff-openai/src/doeff_openai/costs.py | 26 ++++------ .../src/doeff_openai/effects/cost.py | 1 - .../src/doeff_openai/embeddings.py | 32 +++++------- .../src/doeff_openai/streaming.py | 9 ++-- .../src/doeff_openai/structured_llm.py | 50 +++++++------------ packages/doeff-openai/tests/_runner.py | 3 +- packages/doeff-openai/tests/e2e/conftest.py | 2 +- .../tests/e2e/test_effect_handlers_e2e.py | 11 ++-- .../doeff-openai/tests/e2e/test_openai_api.py | 11 ++-- packages/doeff-openai/tests/unit/conftest.py | 2 +- .../tests/unit/test_effect_handlers.py | 5 +- .../tests/unit/test_openai_effects.py | 3 +- .../doeff-openai/tests/unit/test_pricing.py | 5 +- .../tests/unit/test_retry_tracking.py | 3 +- .../tests/unit/test_simple_retry.py | 3 +- .../tests/unit/test_structured_llm.py | 3 +- 20 files changed, 92 insertions(+), 125 deletions(-) diff --git a/packages/doeff-openai/examples/structured_llm_example.py b/packages/doeff-openai/examples/structured_llm_example.py index bc8ca83f4..f15e930d6 100644 --- a/packages/doeff-openai/examples/structured_llm_example.py +++ b/packages/doeff-openai/examples/structured_llm_example.py @@ -119,7 +119,7 @@ def example_gpt5_reasoning() -> EffectGenerator[MathProblem]: yield slog(step="example3", model=model_name) problem_text = """Solve this problem step by step: - + A train leaves Station A at 10:00 AM traveling at 60 mph. Another train leaves Station B at 10:30 AM traveling at 80 mph toward Station A. The stations are 280 miles apart. @@ -285,11 +285,11 @@ async def a_run_all_examples( ) -> None: """ Run all structured LLM examples. - + Args: openai_api_key: Your OpenAI API key gpt5_available: Whether GPT-5 models are available - + Usage: python -m pinjected run doeff_openai.examples.structured_llm_example.a_run_all_examples \ --openai-api-key "sk-..." \ diff --git a/packages/doeff-openai/src/doeff_openai/__init__.py b/packages/doeff-openai/src/doeff_openai/__init__.py index 99a17fe05..9810a12dc 100644 --- a/packages/doeff-openai/src/doeff_openai/__init__.py +++ b/packages/doeff-openai/src/doeff_openai/__init__.py @@ -12,7 +12,7 @@ Example: >>> from doeff import do, run >>> from doeff_openai import chat_completion, get_total_cost - >>> + >>> >>> @do >>> def my_ai_workflow(): >>> # API key provided via Reader environment @@ -20,13 +20,13 @@ >>> messages=[{"role": "user", "content": "Hello!"}], >>> model="gpt-3.5-turbo" >>> ) - >>> + >>> >>> # Check accumulated cost >>> total_cost = yield get_total_cost() >>> print(f"Total cost: ${total_cost:.4f}") - >>> + >>> >>> return response - >>> + >>> >>> # Run with API key in environment >>> result = run( >>> my_ai_workflow(), @@ -167,9 +167,9 @@ "estimate_max_cost", "extract_request_id", "extract_token_usage", + "get_api_calls", # Cost calculation "get_encoding", - "get_api_calls", "get_model_cost", "get_model_pricing", "get_openai_client", diff --git a/packages/doeff-openai/src/doeff_openai/chat.py b/packages/doeff-openai/src/doeff_openai/chat.py index adb2ee247..705a4d5df 100644 --- a/packages/doeff-openai/src/doeff_openai/chat.py +++ b/packages/doeff-openai/src/doeff_openai/chat.py @@ -4,16 +4,14 @@ import asyncio import time -from collections.abc import AsyncIterator -from typing import Any, Generator +from collections.abc import AsyncIterator, Generator +from typing import Any +from doeff_core_effects import Await, Tell, Try from openai.types.chat import ChatCompletion, ChatCompletionChunk from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam from doeff import do -from doeff_core_effects import Await, Tell, Try - -EffectGenerator = Generator from doeff_openai.client import get_openai_client, track_api_call from doeff_openai.costs import ( calculate_cost, @@ -25,9 +23,10 @@ TokenUsage, ) +EffectGenerator = Generator @do -def chat_completion( +def chat_completion( # noqa: PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged messages: list[dict[str, Any] | ChatCompletionMessageParam], model: str = "gpt-3.5-turbo", temperature: float | None = None, @@ -122,7 +121,7 @@ async def create_stream(): yield Tell("Started streaming chat completion") # Track the streaming request (no immediate response data) - metadata = yield track_api_call( + yield track_api_call( operation="chat.completion", model=model, request_payload=request_data, @@ -138,7 +137,7 @@ async def create_stream(): response = yield Await(client.async_client.chat.completions.create(**request_data)) # Track successful API call - metadata = yield track_api_call( + yield track_api_call( operation="chat.completion", model=model, request_payload=request_data, @@ -162,7 +161,7 @@ async def create_stream(): if safe_result.is_err(): # Track failed API call attempt (tracking will log the error) e = safe_result.error - metadata = yield track_api_call( + yield track_api_call( operation="chat.completion", model=model, request_payload=request_data, @@ -238,7 +237,7 @@ async def create_completion(): response = yield Await(create_completion()) # Track the API call with full metadata - metadata = yield track_api_call( + yield track_api_call( operation="chat.completion", model=model, request_payload=request_data, @@ -254,7 +253,7 @@ async def create_completion(): if safe_result.is_err(): # Track error e = safe_result.error - metadata = yield track_api_call( + yield track_api_call( operation="chat.completion", model=model, request_payload=request_data, diff --git a/packages/doeff-openai/src/doeff_openai/client.py b/packages/doeff-openai/src/doeff_openai/client.py index a149993e6..117a8b4f9 100644 --- a/packages/doeff-openai/src/doeff_openai/client.py +++ b/packages/doeff-openai/src/doeff_openai/client.py @@ -4,23 +4,22 @@ import copy import time +from collections.abc import Generator from dataclasses import dataclass from datetime import datetime, timezone -from typing import Any, Generator +from typing import Any +from doeff_core_effects import Ask, Get, Put, Tell, Try from openai import AsyncOpenAI, OpenAI from openai.types import CreateEmbeddingResponse from openai.types.chat import ChatCompletion from doeff import do -from doeff_core_effects import Ask, Get, Put, Tell, Try - -EffectGenerator = Generator from doeff_openai.effects.cost import CalculateCost from doeff_openai.types import APICallMetadata, TokenUsage -def _prepare_prompt_details( +def _prepare_prompt_details( # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged request_payload: dict[str, Any], ) -> tuple[dict[str, Any], str | None, list[dict[str, Any]], list[dict[str, Any]] | None]: """Create a sanitized payload and extract prompt text/images/messages.""" @@ -138,6 +137,8 @@ def async_client(self) -> AsyncOpenAI: return self._mut_clients.async_ +EffectGenerator = Generator + @do def _get_state_or_none(key: str) -> EffectGenerator[Any | None]: """Read a state key, returning None when it does not exist.""" diff --git a/packages/doeff-openai/src/doeff_openai/costs.py b/packages/doeff-openai/src/doeff_openai/costs.py index e7e7fe43a..bd25b1285 100644 --- a/packages/doeff-openai/src/doeff_openai/costs.py +++ b/packages/doeff-openai/src/doeff_openai/costs.py @@ -34,13 +34,13 @@ def count_tokens(text: str, model: str = "gpt-3.5-turbo") -> int: return len(encoding.encode(text)) -def count_message_tokens( +def count_message_tokens( # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged messages: list[dict[str, Any]], model: str = "gpt-3.5-turbo" ) -> int: """ Count tokens in a list of messages for chat completion. - + Based on OpenAI's guidelines for counting tokens in chat messages. """ encoding = get_encoding(model) @@ -78,20 +78,20 @@ def count_message_tokens( num_tokens += 85 # Base cost for an image elif key == "name": num_tokens += tokens_per_name + len(encoding.encode(value)) - elif key == "function_call" or key == "tool_calls": + elif key in {"function_call", "tool_calls"}: # Function calls and tool calls if isinstance(value, dict): - for k, v in value.items(): + for _k, v in value.items(): if isinstance(v, str): num_tokens += len(encoding.encode(v)) elif isinstance(value, list): for item in value: if isinstance(item, dict): - for k, v in item.items(): + for _k, v in item.items(): if isinstance(v, str): num_tokens += len(encoding.encode(v)) elif isinstance(v, dict): - for kk, vv in v.items(): + for _kk, vv in v.items(): if isinstance(vv, str): num_tokens += len(encoding.encode(vv)) @@ -102,7 +102,7 @@ def count_message_tokens( def count_embedding_tokens( - input: str | list[str], + input: str | list[str], # noqa: A002 - public API parameter name is intentionally stable model: str = "text-embedding-3-small" ) -> int: """Count tokens for embedding input.""" @@ -195,7 +195,7 @@ def estimate_cost( ) -> CostInfo: """ Estimate the cost of an API call. - + Can provide either: - input_text and output_text for simple text - messages for chat completion @@ -211,10 +211,7 @@ def estimate_cost( input_tokens = 0 if output_tokens is None: - if output_text: - output_tokens = count_tokens(output_text, model) - else: - output_tokens = 0 + output_tokens = count_tokens(output_text, model) if output_text else 0 token_usage = TokenUsage( prompt_tokens=input_tokens, @@ -250,10 +247,7 @@ def estimate_max_cost( if pricing is None: raise UnknownModelPricingError(model) - if messages: - input_tokens = count_message_tokens(messages, model) - else: - input_tokens = 0 + input_tokens = count_message_tokens(messages, model) if messages else 0 if max_tokens is None: max_tokens = pricing.max_output_tokens or 4096 diff --git a/packages/doeff-openai/src/doeff_openai/effects/cost.py b/packages/doeff-openai/src/doeff_openai/effects/cost.py index 40dbbd3a3..afaf51eaf 100644 --- a/packages/doeff-openai/src/doeff_openai/effects/cost.py +++ b/packages/doeff-openai/src/doeff_openai/effects/cost.py @@ -12,7 +12,6 @@ from dataclasses import dataclass from doeff import EffectBase - from doeff_openai.types import TokenUsage diff --git a/packages/doeff-openai/src/doeff_openai/embeddings.py b/packages/doeff-openai/src/doeff_openai/embeddings.py index f93c534ba..5b245b13a 100644 --- a/packages/doeff-openai/src/doeff_openai/embeddings.py +++ b/packages/doeff-openai/src/doeff_openai/embeddings.py @@ -4,24 +4,24 @@ import asyncio import time -from typing import Any, Generator, Literal +from collections.abc import Generator +from typing import Any, Literal -from openai.types import CreateEmbeddingResponse - -from doeff import do from doeff_core_effects import Await, Tell, Try from doeff_core_effects.scheduler import Gather +from openai.types import CreateEmbeddingResponse -EffectGenerator = Generator +from doeff import do from doeff_openai.client import get_openai_client, track_api_call from doeff_openai.costs import ( count_embedding_tokens, ) +EffectGenerator = Generator @do def create_embedding( - input: str | list[str], + input: str | list[str], # noqa: A002 - public API parameter name is intentionally stable model: str = "text-embedding-3-small", encoding_format: Literal["float", "base64"] | None = None, dimensions: int | None = None, @@ -74,7 +74,7 @@ def api_call_with_tracking(): response = yield Await(client.async_client.embeddings.create(**request_data)) # Track successful API call - metadata = yield track_api_call( + yield track_api_call( operation="embedding", model=model, request_payload=request_data, @@ -95,7 +95,7 @@ def api_call_with_tracking(): if safe_result.is_err(): # Track failed API call attempt (tracking will log the error) e = safe_result.error - metadata = yield track_api_call( + yield track_api_call( operation="embedding", model=model, request_payload=request_data, @@ -130,7 +130,7 @@ def api_call_with_tracking(): @do def create_embedding_async( - input: str | list[str], + input: str | list[str], # noqa: A002 - public API parameter name is intentionally stable model: str = "text-embedding-3-small", **kwargs: Any, ) -> EffectGenerator[CreateEmbeddingResponse]: @@ -164,7 +164,7 @@ async def create_embeddings(): response = yield Await(create_embeddings()) # Track the API call with full metadata - metadata = yield track_api_call( + yield track_api_call( operation="embedding", model=model, request_payload=request_data, @@ -180,7 +180,7 @@ async def create_embeddings(): if safe_result.is_err(): # Track error e = safe_result.error - metadata = yield track_api_call( + yield track_api_call( operation="embedding", model=model, request_payload=request_data, @@ -275,10 +275,7 @@ def cosine_similarity( norm1 = sum(a * a for a in embedding1) ** 0.5 norm2 = sum(b * b for b in embedding2) ** 0.5 - if norm1 == 0 or norm2 == 0: - similarity = 0.0 - else: - similarity = dot_product / (norm1 * norm2) + similarity = 0.0 if norm1 == 0 or norm2 == 0 else dot_product / (norm1 * norm2) yield Tell(f"Cosine similarity: {similarity:.4f}") @@ -315,10 +312,7 @@ def semantic_search( norm1 = sum(a * a for a in query_embedding) ** 0.5 norm2 = sum(b * b for b in doc_embedding) ** 0.5 - if norm1 > 0 and norm2 > 0: - similarity = dot_product / (norm1 * norm2) - else: - similarity = 0.0 + similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0 similarities.append((i, similarity, documents[i])) diff --git a/packages/doeff-openai/src/doeff_openai/streaming.py b/packages/doeff-openai/src/doeff_openai/streaming.py index 826e1727e..96674a027 100644 --- a/packages/doeff-openai/src/doeff_openai/streaming.py +++ b/packages/doeff-openai/src/doeff_openai/streaming.py @@ -3,15 +3,13 @@ from __future__ import annotations import time -from collections.abc import AsyncIterator, Callable -from typing import Any, Generator +from collections.abc import AsyncIterator, Callable, Generator +from typing import Any +from doeff_core_effects import Await, Get, Put, Tell, Try from openai.types.chat import ChatCompletionChunk from doeff import do -from doeff_core_effects import Await, Get, Put, Tell, Try - -EffectGenerator = Generator from doeff_openai.costs import ( calculate_cost, count_tokens, @@ -21,6 +19,7 @@ TokenUsage, ) +EffectGenerator = Generator @do def process_stream( diff --git a/packages/doeff-openai/src/doeff_openai/structured_llm.py b/packages/doeff-openai/src/doeff_openai/structured_llm.py index 77a3a32a3..6f08aedc5 100644 --- a/packages/doeff-openai/src/doeff_openai/structured_llm.py +++ b/packages/doeff-openai/src/doeff_openai/structured_llm.py @@ -12,15 +12,14 @@ import io import json import time -from typing import Any, Generator +from collections.abc import Generator +from typing import Any import PIL.Image +from doeff_core_effects import Await, Tell, Try from pydantic import BaseModel from doeff import do -from doeff_core_effects import Await, Tell, Try - -EffectGenerator = Generator from doeff_openai.client import ( get_openai_client, track_api_call, @@ -37,15 +36,11 @@ def requires_max_completion_tokens(model: str) -> bool: model_lower = model.lower() # GPT-5 and o1/o3/o4 models require max_completion_tokens return ( - "gpt-5" in model_lower - or "gpt5" in model_lower - or model_lower.startswith("o1") - or model_lower.startswith("o3") - or model_lower.startswith("o4") + "gpt-5" in model_lower or "gpt5" in model_lower or model_lower.startswith(("o1", "o3", "o4")) ) -def convert_pil_to_base64(image: "PIL.Image.Image") -> str: +def convert_pil_to_base64(image: PIL.Image.Image) -> str: """Convert PIL image to base64 string for OpenAI API.""" # Convert PIL image to bytes buffered = io.BytesIO() @@ -57,7 +52,7 @@ def convert_pil_to_base64(image: "PIL.Image.Image") -> str: return f"data:image/png;base64,{img_base64}" -def _collect_message_content_parts(content: Any) -> tuple[Any | None, list[str]]: +def _collect_message_content_parts(content: Any) -> tuple[Any | None, list[str]]: # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged """Extract JSON payload (if any) and text fragments from a message content.""" if content is None: return None, [] @@ -74,10 +69,7 @@ def _collect_message_content_parts(content: Any) -> tuple[Any | None, list[str]] # Extract possible JSON payload part_json = None - if isinstance(part, dict): - part_json = part.get("json") - else: - part_json = getattr(part, "json", None) + part_json = part.get("json") if isinstance(part, dict) else getattr(part, "json", None) if part_json is not None and json_payload is None: json_payload = part_json @@ -194,10 +186,12 @@ def ensure_strict_schema(schema: dict) -> dict: return result +EffectGenerator = Generator + @do def build_messages( text: str, - images: list["PIL.Image.Image"] | None = None, + images: list[PIL.Image.Image] | None = None, detail: str = "auto", ) -> EffectGenerator[list[dict[str, Any]]]: """ @@ -244,7 +238,7 @@ def build_messages( @do -def build_api_parameters( +def build_api_parameters( # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged model: str, messages: list[dict[str, Any]], temperature: float, @@ -286,10 +280,7 @@ def build_api_parameters( # Handle temperature if ( - is_gpt5_model(model) - or model_lower.startswith("o1") - or model_lower.startswith("o3") - or model_lower.startswith("o4") + is_gpt5_model(model) or model_lower.startswith(("o1", "o3", "o4")) ): # These models only support default temperature (1.0) if temperature != 1.0: @@ -423,10 +414,7 @@ def process_structured_response( def parse_json(): yield Tell(f"Parsing JSON response for {response_format.__name__}") - if isinstance(parse_source, str): - parsed_json = json.loads(parse_source) - else: - parsed_json = parse_source + parsed_json = json.loads(parse_source) if isinstance(parse_source, str) else parse_source if hasattr(response_format, "model_validate"): result_model = response_format.model_validate(parsed_json) # type: ignore[attr-defined] @@ -473,7 +461,7 @@ def process_unstructured_response(response: Any) -> EffectGenerator[str]: def structured_llm__openai( text: str, model: str = "gpt-4o", - images: list["PIL.Image.Image"] | None = None, + images: list[PIL.Image.Image] | None = None, response_format: type[BaseModel] | None = None, max_tokens: int = 8192, temperature: float = 0.7, @@ -581,7 +569,7 @@ def api_call_with_tracking(): response = yield Await(client.async_client.chat.completions.create(**api_params)) # Track successful API call - metadata = yield track_api_call( + yield track_api_call( operation="structured_llm", model=model, request_payload=api_params, @@ -596,7 +584,7 @@ def api_call_with_tracking(): if safe_result.is_err(): error = safe_result.error # Track failed API call attempt (tracking will log the error) - metadata = yield track_api_call( + yield track_api_call( operation="structured_llm", model=model, request_payload=api_params, @@ -653,7 +641,7 @@ def api_call_with_tracking(): @do def gpt4o_structured( text: str, - images: list["PIL.Image.Image"] | None = None, + images: list[PIL.Image.Image] | None = None, response_format: type[BaseModel] | None = None, **kwargs, ) -> EffectGenerator[Any]: @@ -672,7 +660,7 @@ def gpt4o_structured( @do def gpt5_nano_structured( text: str, - images: list["PIL.Image.Image"] | None = None, + images: list[PIL.Image.Image] | None = None, response_format: type[BaseModel] | None = None, reasoning_effort: str = "minimal", **kwargs, @@ -697,7 +685,7 @@ def gpt5_nano_structured( @do def gpt5_structured( text: str, - images: list["PIL.Image.Image"] | None = None, + images: list[PIL.Image.Image] | None = None, response_format: type[BaseModel] | None = None, reasoning_effort: str = "medium", **kwargs, diff --git a/packages/doeff-openai/tests/_runner.py b/packages/doeff-openai/tests/_runner.py index a9ff2f515..26ad94226 100644 --- a/packages/doeff-openai/tests/_runner.py +++ b/packages/doeff-openai/tests/_runner.py @@ -20,7 +20,6 @@ from pathlib import Path from typing import Any -from doeff import AskEffect, Pass, Resume, WithHandler, do, run from doeff_core_effects.effects import Try from doeff_core_effects.handlers import ( await_handler, @@ -36,6 +35,8 @@ from doeff_openai.handlers import calculate_cost_handler from doeff_vm import Err, Ok +from doeff import AskEffect, Pass, Resume, WithHandler, do, run + @dataclass class RunResult: diff --git a/packages/doeff-openai/tests/e2e/conftest.py b/packages/doeff-openai/tests/e2e/conftest.py index 4b4ba887f..2ed761250 100644 --- a/packages/doeff-openai/tests/e2e/conftest.py +++ b/packages/doeff-openai/tests/e2e/conftest.py @@ -1,6 +1,6 @@ """Pytest conftest hook — exposes the shared ``run_program`` helper to e2e tests.""" -from pathlib import Path import sys +from pathlib import Path TESTS_DIR = Path(__file__).resolve().parent.parent if str(TESTS_DIR) not in sys.path: 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 e48be6010..05fea306a 100644 --- a/packages/doeff-openai/tests/e2e/test_effect_handlers_e2e.py +++ b/packages/doeff-openai/tests/e2e/test_effect_handlers_e2e.py @@ -4,6 +4,11 @@ import os # noqa: PINJ050 — only for the RUN_OPENAI_E2E gate; the API key itself flows through a handler below import pytest +from _runner import ( + doeff_py_has_openai_key, + openai_api_key_from_doeff_py_handler, + run_program, +) from doeff_openai.effects import ChatCompletion as ChatCompletionEffect from doeff_openai.effects import StructuredOutput from doeff_openai.handlers import production_handlers @@ -11,12 +16,6 @@ from doeff import EffectGenerator, WithHandler, do -from _runner import ( - doeff_py_has_openai_key, - openai_api_key_from_doeff_py_handler, - run_program, -) - pytestmark = pytest.mark.e2e diff --git a/packages/doeff-openai/tests/e2e/test_openai_api.py b/packages/doeff-openai/tests/e2e/test_openai_api.py index 66a818833..76d9991d3 100644 --- a/packages/doeff-openai/tests/e2e/test_openai_api.py +++ b/packages/doeff-openai/tests/e2e/test_openai_api.py @@ -13,6 +13,11 @@ from unittest.mock import AsyncMock, Mock import pytest +from _runner import ( + doeff_py_has_openai_key, + openai_api_key_from_doeff_py_handler, + run_program, +) from doeff_openai import ( get_api_calls, gpt4o_structured, @@ -32,12 +37,6 @@ do, ) -from _runner import ( - doeff_py_has_openai_key, - openai_api_key_from_doeff_py_handler, - run_program, -) - # Mark all tests in this module as e2e pytestmark = pytest.mark.e2e diff --git a/packages/doeff-openai/tests/unit/conftest.py b/packages/doeff-openai/tests/unit/conftest.py index 89fa7dac3..d68aaec9a 100644 --- a/packages/doeff-openai/tests/unit/conftest.py +++ b/packages/doeff-openai/tests/unit/conftest.py @@ -5,8 +5,8 @@ parent ``tests/`` dir on ``sys.path`` so ``import _runner`` works from any test under this package. """ -from pathlib import Path import sys +from pathlib import Path TESTS_DIR = Path(__file__).resolve().parent.parent if str(TESTS_DIR) not in sys.path: diff --git a/packages/doeff-openai/tests/unit/test_effect_handlers.py b/packages/doeff-openai/tests/unit/test_effect_handlers.py index c5337e76b..e6f724922 100644 --- a/packages/doeff-openai/tests/unit/test_effect_handlers.py +++ b/packages/doeff-openai/tests/unit/test_effect_handlers.py @@ -4,6 +4,8 @@ from typing import Any import pytest +from _runner import run_program +from doeff_core_effects.effects import EffectBase from doeff_llm.effects import ( LLMChat, LLMEmbedding, @@ -37,9 +39,6 @@ WithHandler, do, ) -from doeff_core_effects.effects import EffectBase - -from _runner import run_program class StructuredAnswer(BaseModel): diff --git a/packages/doeff-openai/tests/unit/test_openai_effects.py b/packages/doeff-openai/tests/unit/test_openai_effects.py index d0926e59b..363b1da09 100644 --- a/packages/doeff-openai/tests/unit/test_openai_effects.py +++ b/packages/doeff-openai/tests/unit/test_openai_effects.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock import pytest +from _runner import run_program from doeff_openai import ( OpenAIClient, TokenUsage, @@ -29,8 +30,6 @@ do, ) -from _runner import run_program - @pytest.fixture def mock_openai_client(): diff --git a/packages/doeff-openai/tests/unit/test_pricing.py b/packages/doeff-openai/tests/unit/test_pricing.py index 138b0be91..8468cfeb1 100644 --- a/packages/doeff-openai/tests/unit/test_pricing.py +++ b/packages/doeff-openai/tests/unit/test_pricing.py @@ -7,12 +7,10 @@ """ import pytest - -from doeff import Pass, Resume, WithHandler, do, run from doeff_openai import ( + MODEL_PRICING, CalculateCost, CostInfo, - MODEL_PRICING, MissingCachedPricingError, TokenUsage, UnknownModelPricingError, @@ -20,6 +18,7 @@ ) from doeff_openai.handlers.production import openai_production_handler +from doeff import Pass, Resume, WithHandler, do, run # --------------------------------------------------------------------------- # Pricing table — GPT-5 family exists and matches published rates diff --git a/packages/doeff-openai/tests/unit/test_retry_tracking.py b/packages/doeff-openai/tests/unit/test_retry_tracking.py index ceb1a0e14..0e79a2856 100644 --- a/packages/doeff-openai/tests/unit/test_retry_tracking.py +++ b/packages/doeff-openai/tests/unit/test_retry_tracking.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest +from _runner import run_program from doeff_openai import get_api_calls, structured_llm__openai from doeff_openai.client import OpenAIClient @@ -13,8 +14,6 @@ do, ) -from _runner import run_program - @pytest.mark.asyncio async def test_retry_tracking_on_failure_then_success(): diff --git a/packages/doeff-openai/tests/unit/test_simple_retry.py b/packages/doeff-openai/tests/unit/test_simple_retry.py index d712228b4..7427da623 100644 --- a/packages/doeff-openai/tests/unit/test_simple_retry.py +++ b/packages/doeff-openai/tests/unit/test_simple_retry.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock import pytest +from _runner import run_program from doeff_openai import get_api_calls, structured_llm__openai from doeff_openai.client import OpenAIClient @@ -14,8 +15,6 @@ do, ) -from _runner import run_program - @pytest.mark.asyncio async def test_simple_success(): diff --git a/packages/doeff-openai/tests/unit/test_structured_llm.py b/packages/doeff-openai/tests/unit/test_structured_llm.py index 37f449eb4..b8b36d8dc 100644 --- a/packages/doeff-openai/tests/unit/test_structured_llm.py +++ b/packages/doeff-openai/tests/unit/test_structured_llm.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, Mock import pytest +from _runner import run_program from doeff_openai import ( get_api_calls, gpt5_structured, @@ -33,8 +34,6 @@ do, ) -from _runner import run_program - # Test models for structured output class SimpleResponse(BaseModel): From e147a2b374e843192e46b723f0432d17f023e638 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 20/35] Clean Ruff violations in doeff-openrouter --- packages/doeff-openrouter/src/doeff_openrouter/chat.py | 8 ++------ packages/doeff-openrouter/src/doeff_openrouter/client.py | 2 +- .../src/doeff_openrouter/structured_llm.py | 4 ++-- .../doeff-openrouter/tests/unit/test_effect_handlers.py | 2 +- .../doeff-openrouter/tests/unit/test_structured_llm.py | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/doeff-openrouter/src/doeff_openrouter/chat.py b/packages/doeff-openrouter/src/doeff_openrouter/chat.py index ba5f71f8d..00a56a93e 100644 --- a/packages/doeff-openrouter/src/doeff_openrouter/chat.py +++ b/packages/doeff-openrouter/src/doeff_openrouter/chat.py @@ -32,11 +32,7 @@ def __init__(self, payload: dict[str, Any]): def _requires_max_completion_tokens(model: str) -> bool: model_lower = model.lower() return ( - "gpt-5" in model_lower - or "gpt5" in model_lower - or model_lower.startswith("o1") - or model_lower.startswith("o3") - or model_lower.startswith("o4") + "gpt-5" in model_lower or "gpt5" in model_lower or model_lower.startswith(("o1", "o3", "o4")) ) @@ -100,7 +96,7 @@ def chat_completion( start_time = time.time() @do - def perform() -> EffectGenerator[dict[str, Any]]: + def perform(start_time: float = start_time) -> EffectGenerator[dict[str, Any]]: response_data, response_headers = yield Await( client.a_chat_completions( request_data, diff --git a/packages/doeff-openrouter/src/doeff_openrouter/client.py b/packages/doeff-openrouter/src/doeff_openrouter/client.py index 6f4d4519e..3a5f1d4ff 100644 --- a/packages/doeff-openrouter/src/doeff_openrouter/client.py +++ b/packages/doeff-openrouter/src/doeff_openrouter/client.py @@ -23,7 +23,7 @@ from .types import APICallMetadata, CostInfo, TokenUsage -def _prepare_prompt_details( +def _prepare_prompt_details( # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged request_payload: dict[str, Any], ) -> tuple[dict[str, Any], str | None, list[dict[str, Any]], list[dict[str, Any]] | None]: """Return sanitized payload plus extracted text/images/messages.""" diff --git a/packages/doeff-openrouter/src/doeff_openrouter/structured_llm.py b/packages/doeff-openrouter/src/doeff_openrouter/structured_llm.py index 2e29078b8..d743e4133 100644 --- a/packages/doeff-openrouter/src/doeff_openrouter/structured_llm.py +++ b/packages/doeff-openrouter/src/doeff_openrouter/structured_llm.py @@ -29,7 +29,7 @@ def convert_pil_to_base64(image: "PIL.Image.Image") -> str: return f"data:image/{image_format.lower()};base64,{encoded}" -def _collect_message_content_parts(content: Any) -> tuple[Any | None, list[str]]: +def _collect_message_content_parts(content: Any) -> tuple[Any | None, list[str]]: # noqa: PLR0912 - baseline cleanup keeps existing control flow unchanged """Extract JSON payload (if any) and text fragments from message content.""" if content is None: return None, [] @@ -136,7 +136,7 @@ def ensure_strict_schema(schema: dict[str, Any]) -> dict[str, Any]: ensure_strict_schema(value) if schema_type == "array" and "items" in schema: ensure_strict_schema(schema["items"]) - for key, value in list(schema.items()): + for _key, value in list(schema.items()): if isinstance(value, dict): ensure_strict_schema(value) elif isinstance(value, list): diff --git a/packages/doeff-openrouter/tests/unit/test_effect_handlers.py b/packages/doeff-openrouter/tests/unit/test_effect_handlers.py index 4a2a8a9b5..b4712096b 100644 --- a/packages/doeff-openrouter/tests/unit/test_effect_handlers.py +++ b/packages/doeff-openrouter/tests/unit/test_effect_handlers.py @@ -6,6 +6,7 @@ from typing import Any import pytest +from doeff_core_effects.effects import EffectBase as Effect from doeff_llm.effects import ( LLMChat, LLMEmbedding, @@ -27,7 +28,6 @@ from pydantic import BaseModel from doeff import Delegate, Resume, WithHandler, default_handlers, do, run -from doeff_core_effects.effects import EffectBase as Effect class StructuredPayload(BaseModel): diff --git a/packages/doeff-openrouter/tests/unit/test_structured_llm.py b/packages/doeff-openrouter/tests/unit/test_structured_llm.py index 2a4528bd7..415187221 100644 --- a/packages/doeff-openrouter/tests/unit/test_structured_llm.py +++ b/packages/doeff-openrouter/tests/unit/test_structured_llm.py @@ -12,7 +12,7 @@ from doeff import EffectGenerator, default_handlers, do, run structured_llm_module = importlib.import_module("doeff_openrouter.structured_llm") -from doeff_openrouter.structured_llm import ( +from doeff_openrouter.structured_llm import ( # noqa: E402 - late import preserves existing import/setup order StructuredOutputParsingError, build_messages, ensure_strict_schema, From 5dead2de17ca74b506fd79ffe7f624e2c2492798 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 21/35] Clean Ruff violations in doeff-preset --- packages/doeff-preset/tests/test_preset_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/doeff-preset/tests/test_preset_handlers.py b/packages/doeff-preset/tests/test_preset_handlers.py index f8dd43522..eea2b7b60 100644 --- a/packages/doeff-preset/tests/test_preset_handlers.py +++ b/packages/doeff-preset/tests/test_preset_handlers.py @@ -38,12 +38,12 @@ MissingEnvKeyError, Pass, Resume, + Tell, WithHandler, WriterTellEffect, async_run, default_async_handlers, default_handlers, - Tell, do, run, slog, From 66ee387e66b97d37545152deb93a9fc23ab8fc36 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 22/35] Clean Ruff violations in doeff-seedream --- .../src/doeff_seedream/client.py | 13 ++++++------- .../src/doeff_seedream/handlers/production.py | 4 ++-- .../src/doeff_seedream/handlers/testing.py | 2 +- .../src/doeff_seedream/structured_llm.py | 18 ++++-------------- .../doeff-seedream/src/doeff_seedream/types.py | 4 ++-- .../doeff-seedream/tests/test_edit_image.py | 2 +- .../tests/test_effect_handlers.py | 1 + 7 files changed, 17 insertions(+), 27 deletions(-) diff --git a/packages/doeff-seedream/src/doeff_seedream/client.py b/packages/doeff-seedream/src/doeff_seedream/client.py index 2b47d9afb..482b48e66 100644 --- a/packages/doeff-seedream/src/doeff_seedream/client.py +++ b/packages/doeff-seedream/src/doeff_seedream/client.py @@ -54,7 +54,7 @@ class SeedreamClient: The client is intentionally lightweight: it only knows how to POST JSON payloads to ``/images/generations`` and leaves retry, logging, and higher-level orchestration to ``structured_llm``. Instances can be reused - safely across requests – every method is stateless and builds up the + safely across requests - every method is stateless and builds up the payload for each call. """ @@ -129,11 +129,11 @@ def get_seedream_client() -> EffectGenerator[SeedreamClient]: The resolver checks the following keys (in order) before falling back to constructing a new client: - - ``seedream_client`` – expected to be a ready-to-use :class:`SeedreamClient` + - ``seedream_client`` - expected to be a ready-to-use :class:`SeedreamClient` instance. - - ``seedream_api_key`` – string API key used to authorise requests. - - ``seedream_base_url`` – override for self-hosted or staging Ark endpoints. - - ``seedream_default_headers`` – additional HTTP headers to merge into each + - ``seedream_api_key`` - string API key used to authorise requests. + - ``seedream_base_url`` - override for self-hosted or staging Ark endpoints. + - ``seedream_default_headers`` - additional HTTP headers to merge into each request (for example to set ``X-Volc-Region``). """ @@ -237,8 +237,7 @@ def track_api_call( meta["error"] = repr(error) yield Tell( - "Seedream %s %s in %.0f ms" - % ( + "Seedream {} {} in {:.0f} ms".format( operation, "failed" if error else "succeeded", latency_ms, diff --git a/packages/doeff-seedream/src/doeff_seedream/handlers/production.py b/packages/doeff-seedream/src/doeff_seedream/handlers/production.py index 20a10659a..102703f83 100644 --- a/packages/doeff-seedream/src/doeff_seedream/handlers/production.py +++ b/packages/doeff-seedream/src/doeff_seedream/handlers/production.py @@ -4,11 +4,11 @@ from collections.abc import Callable from typing import Any +from doeff_core_effects.effects import EffectBase as Effect from doeff_image.effects import ImageEdit, ImageGenerate from doeff_image.types import ImageResult from doeff import EffectGenerator, Pass, Resume, do -from doeff_core_effects.effects import EffectBase as Effect from doeff_seedream.effects import SeedreamGenerate, SeedreamStructuredOutput from doeff_seedream.structured_llm import _edit_image__seedream4_impl from doeff_seedream.types import SeedreamImageEditResult @@ -109,7 +109,7 @@ def _image_edit_impl(effect: ImageEdit) -> EffectGenerator[ImageResult]: @do -def seedream_image_handler(effect: Effect, k: Any): # noqa: PLR0911 +def seedream_image_handler(effect: Effect, k: Any): """Protocol handler with model routing for unified image effects.""" if isinstance(effect, SeedreamGenerate): if not _is_seedream_model(effect.model): diff --git a/packages/doeff-seedream/src/doeff_seedream/handlers/testing.py b/packages/doeff-seedream/src/doeff_seedream/handlers/testing.py index 0abc656b6..54c36208d 100644 --- a/packages/doeff-seedream/src/doeff_seedream/handlers/testing.py +++ b/packages/doeff-seedream/src/doeff_seedream/handlers/testing.py @@ -8,12 +8,12 @@ from dataclasses import dataclass, field from typing import Any, get_args, get_origin +from doeff_core_effects.effects import EffectBase as Effect from doeff_image.effects import ImageEdit, ImageGenerate from doeff_image.types import ImageResult from PIL import Image as PILImage from doeff import Delegate, Resume, do -from doeff_core_effects.effects import EffectBase as Effect from doeff_seedream.effects import SeedreamGenerate, SeedreamStructuredOutput from doeff_seedream.types import SeedreamImage, SeedreamImageEditResult diff --git a/packages/doeff-seedream/src/doeff_seedream/structured_llm.py b/packages/doeff-seedream/src/doeff_seedream/structured_llm.py index 2c793dbe2..3dbe3f0c5 100644 --- a/packages/doeff-seedream/src/doeff_seedream/structured_llm.py +++ b/packages/doeff-seedream/src/doeff_seedream/structured_llm.py @@ -128,7 +128,7 @@ def _decode_images( results.append( SeedreamImage( image_bytes=image_bytes, - mime_type="image/jpeg" if expected_format == "b64_json" else "image/jpeg", + mime_type="image/jpeg", url=url_value if isinstance(url_value, str) else None, size=size_value, ) @@ -139,7 +139,7 @@ def _decode_images( @do -def _edit_image__seedream4_impl( +def _edit_image__seedream4_impl( # noqa: PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged prompt: str, model: str = DEFAULT_MODEL, images: list[Image.Image] | None = None, @@ -260,11 +260,7 @@ def ask_optional(name: str): response_format = str(payload.get("response_format", DEFAULT_RESPONSE_FORMAT)) yield Tell( - "Preparing Seedream image request using model=%s with %d reference image(s)" - % ( - model, - len(images) if images else 0, - ) + f"Preparing Seedream image request using model={model} with {len(images) if images else 0} reference image(s)" ) client = yield get_seedream_client() @@ -392,13 +388,7 @@ def api_call() -> EffectGenerator[dict[str, Any]]: yield Put("seedream_api_calls", entries) yield Tell( - "Seedream estimated cost $%.4f for %d image(s) (%s); cumulative total $%.4f" - % ( - cost_estimate.total_cost, - cost_estimate.generated_images, - cost_estimate.source, - new_total, - ) + f"Seedream estimated cost ${cost_estimate.total_cost:.4f} for {cost_estimate.generated_images} image(s) ({cost_estimate.source}); cumulative total ${new_total:.4f}" ) return result diff --git a/packages/doeff-seedream/src/doeff_seedream/types.py b/packages/doeff-seedream/src/doeff_seedream/types.py index 9b072f326..3675f3a38 100644 --- a/packages/doeff-seedream/src/doeff_seedream/types.py +++ b/packages/doeff-seedream/src/doeff_seedream/types.py @@ -29,7 +29,7 @@ def to_pil_image(self): # type: ignore[override] image = Image.open(buffer) return image.copy() - def save(self, path: str, *, format: str | None = None) -> None: + def save(self, path: str, *, format: str | None = None) -> None: # noqa: A002 - public API parameter name is intentionally stable """Persist the image to disk.""" image = self.to_pil_image() @@ -60,7 +60,7 @@ def image_bytes(self) -> bytes | None: return None return self.images[0].image_bytes - def save(self, path: str, *, format: str | None = None) -> None: + def save(self, path: str, *, format: str | None = None) -> None: # noqa: A002 - public API parameter name is intentionally stable """Persist the primary image to disk.""" image = self.to_pil_image() diff --git a/packages/doeff-seedream/tests/test_edit_image.py b/packages/doeff-seedream/tests/test_edit_image.py index d650b8710..e6a4b3be5 100644 --- a/packages/doeff-seedream/tests/test_edit_image.py +++ b/packages/doeff-seedream/tests/test_edit_image.py @@ -16,6 +16,7 @@ if str(PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(PACKAGE_ROOT)) +from doeff_core_effects.effects import EffectBase as Effect from doeff_seedream import SeedreamClient, edit_image__seedream4, get_seedream_client from doeff import ( @@ -29,7 +30,6 @@ default_handlers, do, ) -from doeff_core_effects.effects import EffectBase as Effect class RecordingSeedreamClient(SeedreamClient): diff --git a/packages/doeff-seedream/tests/test_effect_handlers.py b/packages/doeff-seedream/tests/test_effect_handlers.py index 9cf48a0a9..c518baa03 100644 --- a/packages/doeff-seedream/tests/test_effect_handlers.py +++ b/packages/doeff-seedream/tests/test_effect_handlers.py @@ -1,6 +1,7 @@ # ruff: noqa: E402 """Tests for Seedream domain effects and handlers.""" +from __future__ import annotations import sys from pathlib import Path From 2500c830c10e4823ce686379e2905d744b92558a Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 23/35] Clean Ruff violations in doeff-test-target --- .../src/doeff_test_target/combinators/advanced.py | 12 +++++------- .../src/doeff_test_target/core/alpha.py | 3 +-- .../src/doeff_test_target/core/beta.py | 3 +-- .../src/doeff_test_target/core/delta.py | 3 +-- .../src/doeff_test_target/core/epsilon.py | 3 +-- .../src/doeff_test_target/core/gamma.py | 3 +-- .../src/doeff_test_target/core/zeta.py | 3 +-- .../src/doeff_test_target/handlers/production.py | 4 ++-- .../src/doeff_test_target/orchestrate.py | 3 +-- .../doeff_test_target/scenarios/comprehensions.py | 5 ++--- .../src/doeff_test_target/scenarios/dataclasses.py | 3 +-- .../src/doeff_test_target/scenarios/decorated.py | 3 +-- .../src/doeff_test_target/scenarios/first_choice.py | 5 ++--- .../src/doeff_test_target/scenarios/intercepting.py | 3 +-- .../src/doeff_test_target/scenarios/lifting.py | 5 ++--- .../src/doeff_test_target/scenarios/methods.py | 5 ++--- .../src/doeff_test_target/scenarios/pattern.py | 5 ++--- .../src/doeff_test_target/scenarios/traverse.py | 7 +++---- .../src/doeff_test_target/scenarios/try_except.py | 3 +-- .../tests/test_with_handler_patterns.py | 7 +++---- 20 files changed, 34 insertions(+), 54 deletions(-) diff --git a/packages/doeff-test-target/src/doeff_test_target/combinators/advanced.py b/packages/doeff-test-target/src/doeff_test_target/combinators/advanced.py index a4694174f..db28b8916 100644 --- a/packages/doeff-test-target/src/doeff_test_target/combinators/advanced.py +++ b/packages/doeff-test-target/src/doeff_test_target/combinators/advanced.py @@ -1,10 +1,8 @@ -from doeff import do -from doeff import Ask, Tell - -from ..core.alpha import helper_alpha -from ..core.beta import helper_beta -from ..core.delta import helper_delta -from ..core.gamma import helper_gamma +from doeff import Ask, Tell, do +from doeff_test_target.core.alpha import helper_alpha +from doeff_test_target.core.beta import helper_beta +from doeff_test_target.core.delta import helper_delta +from doeff_test_target.core.gamma import helper_gamma def helper_eta(): diff --git a/packages/doeff-test-target/src/doeff_test_target/core/alpha.py b/packages/doeff-test-target/src/doeff_test_target/core/alpha.py index d17e93542..25764e1f0 100644 --- a/packages/doeff-test-target/src/doeff_test_target/core/alpha.py +++ b/packages/doeff-test-target/src/doeff_test_target/core/alpha.py @@ -1,5 +1,4 @@ -from doeff import do -from doeff import Ask +from doeff import Ask, do def helper_alpha(): diff --git a/packages/doeff-test-target/src/doeff_test_target/core/beta.py b/packages/doeff-test-target/src/doeff_test_target/core/beta.py index 3b77e945e..66def3fd2 100644 --- a/packages/doeff-test-target/src/doeff_test_target/core/beta.py +++ b/packages/doeff-test-target/src/doeff_test_target/core/beta.py @@ -1,5 +1,4 @@ -from doeff import do -from doeff import Ask, Tell +from doeff import Ask, Tell, do def helper_beta(): diff --git a/packages/doeff-test-target/src/doeff_test_target/core/delta.py b/packages/doeff-test-target/src/doeff_test_target/core/delta.py index 10434655d..7f5a6778b 100644 --- a/packages/doeff-test-target/src/doeff_test_target/core/delta.py +++ b/packages/doeff-test-target/src/doeff_test_target/core/delta.py @@ -1,5 +1,4 @@ -from doeff import do -from doeff import Ask +from doeff import Ask, do def helper_delta(): diff --git a/packages/doeff-test-target/src/doeff_test_target/core/epsilon.py b/packages/doeff-test-target/src/doeff_test_target/core/epsilon.py index d36d518e3..22a16e376 100644 --- a/packages/doeff-test-target/src/doeff_test_target/core/epsilon.py +++ b/packages/doeff-test-target/src/doeff_test_target/core/epsilon.py @@ -1,5 +1,4 @@ -from doeff import do -from doeff import Ask +from doeff import Ask, do def helper_epsilon(): diff --git a/packages/doeff-test-target/src/doeff_test_target/core/gamma.py b/packages/doeff-test-target/src/doeff_test_target/core/gamma.py index f1da0dccc..bf9ec3608 100644 --- a/packages/doeff-test-target/src/doeff_test_target/core/gamma.py +++ b/packages/doeff-test-target/src/doeff_test_target/core/gamma.py @@ -1,5 +1,4 @@ -from doeff import do -from doeff import Ask, Tell +from doeff import Ask, Tell, do def helper_gamma(): diff --git a/packages/doeff-test-target/src/doeff_test_target/core/zeta.py b/packages/doeff-test-target/src/doeff_test_target/core/zeta.py index 65a3164a0..6ff83d377 100644 --- a/packages/doeff-test-target/src/doeff_test_target/core/zeta.py +++ b/packages/doeff-test-target/src/doeff_test_target/core/zeta.py @@ -1,5 +1,4 @@ -from doeff import do -from doeff import Ask +from doeff import Ask, do def helper_zeta(): 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 09af7cbfa..d7fb8bdf1 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 @@ -5,8 +5,8 @@ from dataclasses import dataclass, field from typing import Any -from doeff import EffectBase as Effect, Pass, Resume, do -from doeff import Ask, Tell +from doeff import Ask, Pass, Resume, Tell, do +from doeff import EffectBase as Effect from doeff_test_target.effects import ReadFixtureValue, RecordFixtureEvent ProtocolHandler = Callable[[Any, Any], Any] diff --git a/packages/doeff-test-target/src/doeff_test_target/orchestrate.py b/packages/doeff-test-target/src/doeff_test_target/orchestrate.py index 57bb257f6..18b244a3a 100644 --- a/packages/doeff-test-target/src/doeff_test_target/orchestrate.py +++ b/packages/doeff-test-target/src/doeff_test_target/orchestrate.py @@ -1,5 +1,4 @@ -from doeff import Program, do -from doeff import Tell +from doeff import Program, Tell, do from .combinators.advanced import ( helper_eta, diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/comprehensions.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/comprehensions.py index ac8f60277..4aeec92f9 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/comprehensions.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/comprehensions.py @@ -1,7 +1,6 @@ from doeff import Program, do - -from ..core.alpha import helper_alpha -from ..core.beta import helper_beta +from doeff_test_target.core.alpha import helper_alpha +from doeff_test_target.core.beta import helper_beta @do diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/dataclasses.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/dataclasses.py index 373636244..91a9c64b6 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/dataclasses.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/dataclasses.py @@ -1,8 +1,7 @@ from dataclasses import dataclass from doeff import do - -from ..core.alpha import helper_alpha +from doeff_test_target.core.alpha import helper_alpha @dataclass diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/decorated.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/decorated.py index 8956576ef..a1be5887a 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/decorated.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/decorated.py @@ -1,8 +1,7 @@ from functools import wraps from doeff import do - -from ..core.alpha import helper_alpha +from doeff_test_target.core.alpha import helper_alpha def noop_decorator(func): diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/first_choice.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/first_choice.py index 05abf8500..35100e00c 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/first_choice.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/first_choice.py @@ -1,7 +1,6 @@ from doeff import Maybe, Program, do - -from ..core.alpha import helper_alpha -from ..core.beta import helper_beta +from doeff_test_target.core.alpha import helper_alpha +from doeff_test_target.core.beta import helper_beta @do diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/intercepting.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/intercepting.py index 5134ec0b0..24a957962 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/intercepting.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/intercepting.py @@ -1,6 +1,5 @@ from doeff import do - -from ..core.alpha import helper_alpha +from doeff_test_target.core.alpha import helper_alpha @do diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/lifting.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/lifting.py index f85c3401b..7b9ccfeef 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/lifting.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/lifting.py @@ -1,7 +1,6 @@ from doeff import Program, do - -from ..core.alpha import helper_alpha -from ..core.beta import helper_beta +from doeff_test_target.core.alpha import helper_alpha +from doeff_test_target.core.beta import helper_beta def dict_builder(): diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/methods.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/methods.py index f6003d0ea..20d9d2ef5 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/methods.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/methods.py @@ -1,7 +1,6 @@ from doeff import do - -from ..core.alpha import helper_alpha -from ..core.beta import helper_beta +from doeff_test_target.core.alpha import helper_alpha +from doeff_test_target.core.beta import helper_beta class MethodPrograms: diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/pattern.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/pattern.py index cf75b2230..cef7498db 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/pattern.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/pattern.py @@ -1,7 +1,6 @@ from doeff import do - -from ..core.alpha import helper_alpha -from ..core.beta import helper_beta +from doeff_test_target.core.alpha import helper_alpha +from doeff_test_target.core.beta import helper_beta @do diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/traverse.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/traverse.py index 530116e91..8b56286f7 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/traverse.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/traverse.py @@ -1,8 +1,7 @@ from doeff import Program, do - -from ..core.alpha import helper_alpha -from ..core.beta import helper_beta -from ..core.gamma import helper_gamma +from doeff_test_target.core.alpha import helper_alpha +from doeff_test_target.core.beta import helper_beta +from doeff_test_target.core.gamma import helper_gamma def _builder(name: str): diff --git a/packages/doeff-test-target/src/doeff_test_target/scenarios/try_except.py b/packages/doeff-test-target/src/doeff_test_target/scenarios/try_except.py index f4a5d39be..94d6cca74 100644 --- a/packages/doeff-test-target/src/doeff_test_target/scenarios/try_except.py +++ b/packages/doeff-test-target/src/doeff_test_target/scenarios/try_except.py @@ -1,6 +1,5 @@ from doeff import do - -from ..core.alpha import helper_alpha +from doeff_test_target.core.alpha import helper_alpha @do 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 311fac0ea..7bc126ed4 100644 --- a/packages/doeff-test-target/tests/test_with_handler_patterns.py +++ b/packages/doeff-test-target/tests/test_with_handler_patterns.py @@ -3,9 +3,8 @@ from typing import Any import pytest -from doeff import AskEffect, Effect, Pass, Resume, WithHandler, default_handlers, do, run -from doeff import Ask +from doeff import Ask, AskEffect, Effect, Pass, Resume, WithHandler, default_handlers, do, run sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) @@ -71,7 +70,7 @@ def inner_handler(effect: Effect, k): def test_withhandler_error_propagation_from_handler(): - class HandlerFailure(RuntimeError): + class HandlerFailure(RuntimeError): # noqa: N818 - public or fixture exception name is intentionally stable pass @do @@ -122,7 +121,7 @@ def test_withhandler_resume_supports_various_value_types(): for sample_value in sample_values: @do - def typed_mock_handler(effect: Effect, k): + def typed_mock_handler(effect: Effect, k, sample_value=sample_value): if isinstance(effect, AskEffect): return (yield Resume(k, sample_value)) return (yield Pass()) From 12fa6481944e873d4c38eca0c26ae2373768c62d Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:58 +0900 Subject: [PATCH 24/35] Clean Ruff violations in doeff-time --- .../src/doeff_time/handlers/async_time.py | 3 ++- .../src/doeff_time/handlers/sim_time.py | 5 +++-- .../src/doeff_time/handlers/sync_time.py | 4 +++- packages/doeff-time/tests/conftest.py | 15 ++++++++++++--- packages/doeff-time/tests/test_async_handler.py | 2 +- packages/doeff-time/tests/test_schedule_at.py | 2 +- .../doeff-time/tests/test_sim_daemon_pattern.py | 4 ++-- .../tests/test_sim_migration_readiness.py | 6 +++--- packages/doeff-time/tests/test_sim_time.py | 10 +++++----- packages/doeff-time/tests/test_sync_handler.py | 2 +- packages/doeff-time/tests/test_time_queue.py | 3 +-- 11 files changed, 34 insertions(+), 22 deletions(-) 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 435d321cc..4306cbae8 100644 --- a/packages/doeff-time/src/doeff_time/handlers/async_time.py +++ b/packages/doeff-time/src/doeff_time/handlers/async_time.py @@ -6,9 +6,10 @@ from datetime import datetime, timezone from typing import Any -from doeff import Pass, Resume, do from doeff_core_effects import Await from doeff_core_effects.scheduler import Spawn + +from doeff import Pass, Resume, do from doeff_time.effects import DelayEffect, GetTimeEffect, ScheduleAtEffect, WaitUntilEffect ProtocolHandler = Callable[[Any, Any], Any] diff --git a/packages/doeff-time/src/doeff_time/handlers/sim_time.py b/packages/doeff-time/src/doeff_time/handlers/sim_time.py index f98f9852e..18de7b8dc 100644 --- a/packages/doeff-time/src/doeff_time/handlers/sim_time.py +++ b/packages/doeff-time/src/doeff_time/handlers/sim_time.py @@ -5,15 +5,16 @@ from datetime import datetime, timedelta, timezone from typing import Any -from doeff import Pass, Resume, Transfer, do from doeff_core_effects import WriterTellEffect from doeff_core_effects.scheduler import ( + PRIORITY_IDLE, CompletePromise, CreatePromise, - PRIORITY_IDLE, Spawn, Wait, ) + +from doeff import Pass, Resume, Transfer, do from doeff_time._internals import SimClock, TimeQueue from doeff_time.effects import ( DelayEffect, 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 5ea608654..dbe63b9cc 100644 --- a/packages/doeff-time/src/doeff_time/handlers/sync_time.py +++ b/packages/doeff-time/src/doeff_time/handlers/sync_time.py @@ -7,8 +7,10 @@ from datetime import datetime, timezone from typing import Any +from doeff_core_effects.scheduler import CreateExternalPromise, Spawn +from doeff_core_effects.scheduler import Wait as WaitTask + from doeff import Pass, Resume, do -from doeff_core_effects.scheduler import CreateExternalPromise, Spawn, Wait as WaitTask from doeff_time.effects import DelayEffect, GetTimeEffect, ScheduleAtEffect, WaitUntilEffect ProtocolHandler = Callable[[Any, Any], Any] diff --git a/packages/doeff-time/tests/conftest.py b/packages/doeff-time/tests/conftest.py index 1e423f86b..fb6e2b213 100644 --- a/packages/doeff-time/tests/conftest.py +++ b/packages/doeff-time/tests/conftest.py @@ -28,9 +28,10 @@ def listen(program, types=None): OCaml 5 pattern: local match_with, not an effect. Returns DoExpr that evaluates to (result, collected_effects). """ - from doeff import WithHandler, Pass, Resume, do from doeff_core_effects import WriterTellEffect + from doeff import Pass, WithHandler, do + types_to_collect = types or (WriterTellEffect,) collected = [] @@ -49,10 +50,18 @@ def _listen(): def run_with_handlers(program, *, env=None): - from doeff import WithHandler, run - from doeff_core_effects.handlers import writer, listen_handler, try_handler, slog_handler, reader, await_handler + from doeff_core_effects.handlers import ( + await_handler, + listen_handler, + reader, + slog_handler, + try_handler, + writer, + ) from doeff_core_effects.scheduler import scheduled + from doeff import WithHandler, run + wrapped = program if env is not None: wrapped = WithHandler(reader(env=env), wrapped) diff --git a/packages/doeff-time/tests/test_async_handler.py b/packages/doeff-time/tests/test_async_handler.py index 84e631cb8..f0a2b58cc 100644 --- a/packages/doeff-time/tests/test_async_handler.py +++ b/packages/doeff-time/tests/test_async_handler.py @@ -2,12 +2,12 @@ import time from datetime import datetime, timedelta, timezone +from doeff_core_effects import Ask from doeff_time.effects import Delay, WaitUntil from doeff_time.handlers import async_time_handler from conftest import run_with_handlers from doeff import WithHandler, do -from doeff_core_effects import Ask @do diff --git a/packages/doeff-time/tests/test_schedule_at.py b/packages/doeff-time/tests/test_schedule_at.py index 9beb56f7c..2a3f548a0 100644 --- a/packages/doeff-time/tests/test_schedule_at.py +++ b/packages/doeff-time/tests/test_schedule_at.py @@ -2,12 +2,12 @@ from dataclasses import dataclass from datetime import timedelta +from doeff_core_effects.scheduler import Wait from doeff_time.effects import Delay, GetTime, ScheduleAt 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_core_effects.scheduler import Wait def _make_marker_program(marker: dict[str, bool]): diff --git a/packages/doeff-time/tests/test_sim_daemon_pattern.py b/packages/doeff-time/tests/test_sim_daemon_pattern.py index b4b35e80a..b2a8749b9 100644 --- a/packages/doeff-time/tests/test_sim_daemon_pattern.py +++ b/packages/doeff-time/tests/test_sim_daemon_pattern.py @@ -6,12 +6,12 @@ from typing import Any import pytest -from conftest import SIM_TIME_EPOCH, run_with_handlers, sim_time +from doeff_core_effects.scheduler import Gather, Spawn, Wait from doeff_events import Publish, WaitForEvent, event_handler 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_core_effects.scheduler import Gather, Spawn, Wait def _run_sim( diff --git a/packages/doeff-time/tests/test_sim_migration_readiness.py b/packages/doeff-time/tests/test_sim_migration_readiness.py index ff3b6c48b..62200b63f 100644 --- a/packages/doeff-time/tests/test_sim_migration_readiness.py +++ b/packages/doeff-time/tests/test_sim_migration_readiness.py @@ -25,16 +25,16 @@ from typing import Any import pytest -from conftest import SIM_TIME_EPOCH, listen, run_with_handlers, sim_seconds, sim_time +from doeff_core_effects import Tell +from doeff_core_effects.scheduler import Gather, Race, Spawn, Wait from doeff_events import Publish, WaitForEvent, event_handler from doeff_time import Delay, GetTime, ScheduleAt, SetTime, WaitUntil, sim_time_handler +from conftest import SIM_TIME_EPOCH, listen, run_with_handlers, sim_seconds, sim_time from doeff import ( WithHandler, do, ) -from doeff_core_effects import Listen, Tell -from doeff_core_effects.scheduler import Gather, Race, Spawn, Wait # --------------------------------------------------------------------------- # Test helpers diff --git a/packages/doeff-time/tests/test_sim_time.py b/packages/doeff-time/tests/test_sim_time.py index daaa64c2d..42aaae1b4 100644 --- a/packages/doeff-time/tests/test_sim_time.py +++ b/packages/doeff-time/tests/test_sim_time.py @@ -4,20 +4,20 @@ from datetime import datetime, timedelta, timezone from typing import Any -from conftest import SIM_TIME_EPOCH, listen, run_with_handlers, sim_seconds, sim_time +from doeff_core_effects import Tell +from doeff_core_effects.scheduler import PRIORITY_HIGH, Gather, Spawn, Wait +from doeff_core_effects.scheduler import Gather as GatherEffect +from doeff_core_effects.scheduler import Spawn as SpawnEffect from doeff_events import Publish, WaitForEvent, event_handler from doeff_time import Delay, GetTime, ScheduleAt, SetTime, WaitUntil, sim_time_handler +from conftest import SIM_TIME_EPOCH, listen, run_with_handlers, sim_seconds, sim_time from doeff import ( Effect, Pass, WithHandler, do, ) -from doeff_core_effects import Listen, Tell -from doeff_core_effects.scheduler import Gather, PRIORITY_HIGH, Spawn, Wait -from doeff_core_effects.scheduler import Spawn as SpawnEffect -from doeff_core_effects.scheduler import Gather as GatherEffect def _run_with_sim( diff --git a/packages/doeff-time/tests/test_sync_handler.py b/packages/doeff-time/tests/test_sync_handler.py index 359e2f5e8..469fbc2f4 100644 --- a/packages/doeff-time/tests/test_sync_handler.py +++ b/packages/doeff-time/tests/test_sync_handler.py @@ -2,12 +2,12 @@ import time from datetime import datetime, timedelta, timezone +from doeff_core_effects import Ask from doeff_time.effects import Delay, WaitUntil from doeff_time.handlers import sync_time_handler from conftest import run_with_handlers from doeff import WithHandler, do -from doeff_core_effects import Ask @do diff --git a/packages/doeff-time/tests/test_time_queue.py b/packages/doeff-time/tests/test_time_queue.py index 741359ba4..df955b684 100644 --- a/packages/doeff-time/tests/test_time_queue.py +++ b/packages/doeff-time/tests/test_time_queue.py @@ -1,9 +1,8 @@ from datetime import datetime, timedelta, timezone import pytest -from doeff_time._internals import TimeQueue - from doeff_core_effects.scheduler import Promise +from doeff_time._internals import TimeQueue def _promise(promise_id: int) -> Promise: From 57d93be2f241dd551d7da67c4a00b946f24c4248 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:59 +0900 Subject: [PATCH 25/35] Clean Ruff violations in doeff-traverse --- .../doeff-traverse/doeff_traverse/__init__.py | 46 ++++++++++--- .../doeff-traverse/doeff_traverse/handlers.py | 66 +++++++++++-------- .../tests/test_memory_leak_multi_day.py | 29 ++++---- .../tests/test_traverse_deep_recursion.py | 46 ++++++------- 4 files changed, 113 insertions(+), 74 deletions(-) diff --git a/packages/doeff-traverse/doeff_traverse/__init__.py b/packages/doeff-traverse/doeff_traverse/__init__.py index 8d5eedbc2..741f2b72f 100644 --- a/packages/doeff-traverse/doeff_traverse/__init__.py +++ b/packages/doeff-traverse/doeff_traverse/__init__.py @@ -7,16 +7,44 @@ Comprehension macro (for/do) uses From, When, and Skip internally. """ +from doeff_traverse.collection import Collection as Collection from doeff_traverse.effects import ( - Fail, Traverse, Reduce, Zip, Inspect, - Skip, SortBy, Take, + Fail as Fail, +) +from doeff_traverse.effects import ( + Inspect as Inspect, +) +from doeff_traverse.effects import ( + Reduce as Reduce, +) +from doeff_traverse.effects import ( + Skip as Skip, +) +from doeff_traverse.effects import ( + SortBy as SortBy, +) +from doeff_traverse.effects import ( + Take as Take, +) +from doeff_traverse.effects import ( + Traverse as Traverse, +) +from doeff_traverse.effects import ( + Zip as Zip, +) +from doeff_traverse.handlers import ( + fail_handler as fail_handler, +) +from doeff_traverse.handlers import ( + normalize_to_none as normalize_to_none, +) +from doeff_traverse.handlers import ( + parallel as parallel, +) +from doeff_traverse.handlers import ( + parallel_fail_fast as parallel_fail_fast, ) -from doeff_traverse.helpers import try_call from doeff_traverse.handlers import ( - sequential, - parallel, - parallel_fail_fast, - fail_handler, - normalize_to_none, + sequential as sequential, ) -from doeff_traverse.collection import Collection +from doeff_traverse.helpers import try_call as try_call diff --git a/packages/doeff-traverse/doeff_traverse/handlers.py b/packages/doeff-traverse/doeff_traverse/handlers.py index 31ff8f8e4..f55e3441a 100644 --- a/packages/doeff-traverse/doeff_traverse/handlers.py +++ b/packages/doeff-traverse/doeff_traverse/handlers.py @@ -6,16 +6,15 @@ """ from doeff import do -from doeff.program import Resume, Pass, ResumeThrow - -from doeff_traverse.effects import Fail, Traverse, Reduce, Zip, Inspect, Skip, SortBy, Take +from doeff.program import Pass, Resume, ResumeThrow +from doeff_traverse.collection import Collection, HistoryEntry, ItemResult +from doeff_traverse.effects import Fail, Inspect, Reduce, Skip, SortBy, Take, Traverse, Zip # Sentinel for Skip — traverse handler checks identity _SKIPPED = object() -from doeff_traverse.collection import Collection, ItemResult, HistoryEntry -def sequential(): +def sequential(): # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged """Sequential handler for Traverse/Reduce/Zip/Inspect. Traverse: runs f(item) sequentially for each item. @@ -26,13 +25,16 @@ def sequential(): Unhandled Fail inside Traverse marks the item as failed. """ - from doeff.program import WithHandler as WH - from doeff.handler_utils import get_inner_handlers from doeff_core_effects.effects import Try - from doeff_vm import Ok, Err + 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): + def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, Skip): return _SKIPPED @@ -64,7 +66,7 @@ def handler(effect, k): # Wrap in Try to catch unhandled failures per item @do - def attempt(): + def attempt(prog=prog): from doeff_core_effects.handlers import try_handler value = yield WH(try_handler, Try(prog)) return value @@ -118,7 +120,7 @@ def attempt(): a = Collection.from_iterable(effect.a) b = Collection.from_iterable(effect.b) results = [] - for item_a, item_b in zip(a.all_items, b.all_items): + for item_a, item_b in zip(a.all_items, b.all_items, strict=False): failed = item_a.failed or item_b.failed value = (item_a.value if item_a.failed else item_b.value) if failed else (item_a.value, item_b.value) history = item_a.history + item_b.history @@ -170,7 +172,7 @@ def attempt(): return handler -def parallel(concurrency=10): +def parallel(concurrency=10): # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged """Parallel handler for Traverse/Reduce/Zip/Inspect. Traverse: spawns up to `concurrency` tasks concurrently via Spawn/Gather. @@ -178,17 +180,23 @@ def parallel(concurrency=10): Items from generators are materialized before spawning. """ - from doeff.program import WithHandler as WH - from doeff.handler_utils import get_inner_handlers from doeff_core_effects.effects import Try - from doeff_core_effects.scheduler import Spawn, Gather from doeff_core_effects.scheduler import ( - CreateSemaphore, AcquireSemaphore, ReleaseSemaphore, + AcquireSemaphore, + CreateSemaphore, + Gather, + ReleaseSemaphore, + Spawn, + ) + 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 ) - from doeff_vm import Ok, Err @do - def handler(effect, k): + def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, Skip): return _SKIPPED @@ -291,7 +299,7 @@ def attempt(): a = Collection.from_iterable(effect.a) b = Collection.from_iterable(effect.b) results = [] - for item_a, item_b in zip(a.all_items, b.all_items): + for item_a, item_b in zip(a.all_items, b.all_items, strict=False): failed = item_a.failed or item_b.failed value = (item_a.value if item_a.failed else item_b.value) if failed else (item_a.value, item_b.value) history = item_a.history + item_b.history @@ -341,7 +349,7 @@ def attempt(): return handler -def parallel_fail_fast(concurrency=10): +def parallel_fail_fast(concurrency=10): # noqa: PLR0915 - baseline cleanup keeps existing control flow unchanged """Parallel handler that aborts on first failure. Same interface as parallel(), but does NOT wrap items in Try. @@ -351,15 +359,21 @@ def parallel_fail_fast(concurrency=10): Use this instead of parallel() when silent failure collection is unacceptable. Swap in the interpreter: (parallel :concurrency 40) → (parallel-fail-fast :concurrency 40) """ - from doeff.program import WithHandler as WH - from doeff.handler_utils import get_inner_handlers - from doeff_core_effects.scheduler import Spawn, Gather from doeff_core_effects.scheduler import ( - CreateSemaphore, AcquireSemaphore, ReleaseSemaphore, + AcquireSemaphore, + CreateSemaphore, + Gather, + ReleaseSemaphore, + Spawn, + ) + + 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): + def handler(effect, k): # noqa: PLR0911, PLR0912, PLR0915 - baseline cleanup keeps existing control flow unchanged if isinstance(effect, Skip): return _SKIPPED @@ -446,7 +460,7 @@ def run_item(item=item): a = Collection.from_iterable(effect.a) b = Collection.from_iterable(effect.b) results = [] - for item_a, item_b in zip(a.all_items, b.all_items): + for item_a, item_b in zip(a.all_items, b.all_items, strict=False): failed = item_a.failed or item_b.failed if failed: cause = item_a.value if item_a.failed else item_b.value 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 e275291b6..c2512ce9f 100644 --- a/packages/doeff-traverse/tests/test_memory_leak_multi_day.py +++ b/packages/doeff-traverse/tests/test_memory_leak_multi_day.py @@ -14,15 +14,10 @@ import resource import sys - -import pytest - from dataclasses import dataclass -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone -from doeff import EffectBase, Pass, Program, Resume, do, run, slog -from doeff_vm import WithHandler -from doeff_core_effects import Ask +import pytest from doeff_core_effects.handlers import ( await_handler, lazy_ask, @@ -34,10 +29,12 @@ writer, ) from doeff_core_effects.scheduler import scheduled -from doeff_time import GetTime, WaitUntil from doeff_time.handlers import sim_time_handler from doeff_traverse.effects import Inspect, Traverse -from doeff_traverse.handlers import parallel, fail_handler +from doeff_traverse.handlers import fail_handler, parallel +from doeff_vm import WithHandler + +from doeff import EffectBase, Pass, Program, Resume, do, run, slog SIM_EPOCH = datetime(2025, 1, 1, tzinfo=timezone.utc) @@ -86,7 +83,7 @@ def _single_item_program(item_id: int) -> Program[dict]: def _daily_traverse(n_items: int) -> Program[list]: """Simulates one day's factor computation via traverse. - Production: traverse over 95 days × 24 symbols = many items. + Production: traverse over 95 days x 24 symbols = many items. Each item triggers memo-HIT LLM query. """ collection = yield Traverse(_single_item_program, range(n_items), label="daily_factor") @@ -99,7 +96,7 @@ def _daily_traverse(n_items: int) -> Program[list]: def _multi_day_program(n_days: int, n_items_per_day: int) -> Program[int]: """Simulates multi-day pipeline: loop over days, each running traverse. - Production: 60 days × traverse(95 items × 24 symbols). + Production: 60 days x traverse(95 items x 24 symbols). This is where memory grows unbounded. """ for day in range(n_days): @@ -151,7 +148,7 @@ def test_multi_day_traverse_memory_constant(): If memory leaks, RSS will grow significantly per day. Pass criteria: RSS growth < 50MB over 10 days. - (With leak: ~280MB/day × 10 = 2.8GB growth) + (With leak: ~280MB/day x 10 = 2.8GB growth) """ import gc @@ -174,9 +171,9 @@ def test_multi_day_traverse_memory_constant(): def test_multi_day_traverse_30_days(): - """30 days × 100 items — matches production scale. + """30 days x 100 items — matches production scale. - Production: 30 days × 95 factor days × 24 symbols ≈ 30 × 100. + Production: 30 days x 95 factor days x 24 symbols ≈ 30 x 100. Should complete without OOM on a 16GB machine. """ import gc @@ -200,7 +197,7 @@ def test_multi_day_traverse_30_days(): @pytest.mark.slow def test_multi_day_traverse_60_days_stress(): - """60 days × 200 items — stress test matching production failure. + """60 days x 200 items — stress test matching production failure. Production OOMs at day 53 with 17GB RSS. This test uses 200 items/day to amplify the leak. @@ -218,6 +215,6 @@ def test_multi_day_traverse_60_days_stress(): growth = rss_after - rss_before assert growth < 300, ( - f"Memory grew {growth:.0f}MB over 60 days × 200 items. " + f"Memory grew {growth:.0f}MB over 60 days x 200 items. " f"Expected < 300MB. Production OOMs at ~17GB after 53 days." ) diff --git a/packages/doeff-traverse/tests/test_traverse_deep_recursion.py b/packages/doeff-traverse/tests/test_traverse_deep_recursion.py index 02558d8d5..0300814d4 100644 --- a/packages/doeff-traverse/tests/test_traverse_deep_recursion.py +++ b/packages/doeff-traverse/tests/test_traverse_deep_recursion.py @@ -11,13 +11,10 @@ Expected: traverse over 300+ items should work without deep recursion. """ -import pytest from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from doeff import EffectBase, Pass, Program, Resume, do, run, slog -from doeff_vm import WithHandler from doeff_core_effects import Ask, Get, Put from doeff_core_effects.handlers import ( await_handler, @@ -30,9 +27,12 @@ writer, ) from doeff_core_effects.scheduler import scheduled -from doeff_traverse.effects import Inspect, Traverse -from doeff_traverse.handlers import parallel, sequential, fail_handler from doeff_time import GetTime, WaitUntil, sim_time_handler +from doeff_traverse.effects import Inspect, Traverse +from doeff_traverse.handlers import fail_handler, parallel +from doeff_vm import WithHandler + +from doeff import EffectBase, Pass, Program, Resume, do, run, slog @do @@ -247,7 +247,7 @@ def test_traverse_300_with_sim_time(): def test_traverse_300_multi_effect_with_sim_time(): - """300 items × 3+ effects each + sim_time_handler. + """300 items x 3+ effects each + sim_time_handler. Closest reproduction of production: each traverse item performs GetTime + Ask + slog effects with sim_time_handler active. @@ -273,7 +273,7 @@ def test_traverse_100_waituntil_with_sim_time(): def test_traverse_300_waituntil_with_sim_time(): - """300 items × WaitUntil — stress test for sim_time + traverse. + """300 items x WaitUntil — stress test for sim_time + traverse. This is the most demanding test: 300 concurrent traverse items, each issuing WaitUntil to different sim times, forcing the @@ -289,8 +289,8 @@ def test_traverse_300_waituntil_with_sim_time(): # # Production handler stack has 15+ handlers between the program and the # scheduler. Each effect round-trip traverses the full handler chain. -# With N items × M effects/item × D handler depth, the total continuation -# depth grows as O(N × M × D), eventually causing stack overflow. +# With N items x M effects/item x D handler depth, the total continuation +# depth grows as O(N x M x D), eventually causing stack overflow. def _make_passthrough_handler(tag: str): @@ -316,7 +316,7 @@ def _run_deep_stack(program, n_extra_handlers=15, concurrency=40): → sim_time → sqlite_cache → openai → yahoo_finance → news_minio → news_polygon → sim_exchange → margin → cache → slack → parallel → fail → slog - → memo_rewriters (×4) + → memo_rewriters (x4) That's ~20+ handlers. Each effect in a traverse item must traverse the full chain on every yield. This test simulates that depth. @@ -341,7 +341,7 @@ def _run_deep_stack(program, n_extra_handlers=15, concurrency=40): def test_traverse_100_deep_stack(): - """100 items × GetTime + deep handler stack (15 extra handlers).""" + """100 items x GetTime + deep handler stack (15 extra handlers).""" result = _run_deep_stack( _make_traverse_program(100, process_item_with_time), n_extra_handlers=15, @@ -350,7 +350,7 @@ def test_traverse_100_deep_stack(): def test_traverse_300_deep_stack(): - """300 items × GetTime + deep handler stack (15 extra handlers). + """300 items x GetTime + deep handler stack (15 extra handlers). This is the closest reproduction of the production failure: - 300 items (= 300 trading days in compute_rolling_factors) @@ -366,10 +366,10 @@ def test_traverse_300_deep_stack(): def test_traverse_300_multi_effect_deep_stack(): - """300 items × 3+ effects × deep handler stack. + """300 items x 3+ effects x deep handler stack. - Each item does GetTime + Ask + 2×slog = 4 effect round-trips, - each traversing 20+ handlers. Total continuation depth ~ 300 × 4 × 20. + Each item does GetTime + Ask + 2xslog = 4 effect round-trips, + each traversing 20+ handlers. Total continuation depth ~ 300 x 4 x 20. """ result = _run_deep_stack( _make_traverse_program(300, process_item_time_multi_effect), @@ -380,7 +380,7 @@ def test_traverse_300_multi_effect_deep_stack(): def test_traverse_500_multi_effect_deep_stack(): - """500 items × 3+ effects × deep handler stack — stress test.""" + """500 items x 3+ effects x deep handler stack — stress test.""" result = _run_deep_stack( _make_traverse_program(500, process_item_time_multi_effect), n_extra_handlers=15, @@ -389,7 +389,7 @@ def test_traverse_500_multi_effect_deep_stack(): def test_traverse_300_very_deep_stack(): - """300 items × GetTime + 30 extra handlers — extreme depth. + """300 items x GetTime + 30 extra handlers — extreme depth. Tests whether handler chain depth alone causes issues at scale. """ @@ -430,9 +430,9 @@ def process_item_domain(i: int) -> Program[int]: 3. DomainQuery (handled by domain handler — Resume) 4. slog (handled by slog handler) - 4 effects × handler catching = deeper continuation chain per item. + 4 effects x handler catching = deeper continuation chain per item. """ - now = yield GetTime() + yield GetTime() cached = yield CacheCheckEffect(key=f"item_{i}") if cached is None: result = yield DomainQueryEffect(key=f"item_{i}") @@ -468,7 +468,7 @@ def _run_deep_catching_stack(program, concurrency=40): - 10 passthrough handlers for additional depth """ cache_handler = _make_catching_handler( - CacheCheckEffect, lambda e: None # always cache miss + CacheCheckEffect, lambda _e: None # always cache miss ) domain_handler = _make_catching_handler( DomainQueryEffect, lambda e: hash(e.key) % 100 # deterministic result @@ -496,7 +496,7 @@ def _run_deep_catching_stack(program, concurrency=40): def test_traverse_100_catching_handlers(): - """100 items × domain effects + catching handlers.""" + """100 items x domain effects + catching handlers.""" result = _run_deep_catching_stack( _make_traverse_program(100, process_item_domain) ) @@ -504,7 +504,7 @@ def test_traverse_100_catching_handlers(): def test_traverse_300_catching_handlers(): - """300 items × domain effects + catching handlers. + """300 items x domain effects + catching handlers. Closest to production: 300 items each performing 4 effects (GetTime, CacheCheck, DomainQuery, slog) with handlers that @@ -517,7 +517,7 @@ def test_traverse_300_catching_handlers(): def test_traverse_500_catching_handlers(): - """500 items × domain effects + catching handlers — stress test.""" + """500 items x domain effects + catching handlers — stress test.""" result = _run_deep_catching_stack( _make_traverse_program(500, process_item_domain) ) From 2e39a9113376d3149c440b214770ffc3381ed038 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:59 +0900 Subject: [PATCH 26/35] Clean Ruff violations in doeff-vm --- packages/doeff-vm/doeff_vm/__init__.pyi | 2 +- packages/doeff-vm/tests/test_memory_stats.py | 16 ++++---- packages/doeff-vm/tests/test_pyvm.py | 39 +++++++------------ .../test_traceback_pure_python_frames.py | 9 ++--- 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/packages/doeff-vm/doeff_vm/__init__.pyi b/packages/doeff-vm/doeff_vm/__init__.pyi index d4cd36c26..727b15a6b 100644 --- a/packages/doeff-vm/doeff_vm/__init__.pyi +++ b/packages/doeff-vm/doeff_vm/__init__.pyi @@ -6,7 +6,7 @@ _T = TypeVar("_T") # --- VM --- -class UnhandledEffect(RuntimeError): ... +class UnhandledEffect(RuntimeError): ... # noqa: N818 - public or fixture exception name is intentionally stable class PyVM: def __init__(self) -> None: ... diff --git a/packages/doeff-vm/tests/test_memory_stats.py b/packages/doeff-vm/tests/test_memory_stats.py index 00b5ae990..6315b1963 100644 --- a/packages/doeff-vm/tests/test_memory_stats.py +++ b/packages/doeff-vm/tests/test_memory_stats.py @@ -2,12 +2,14 @@ from pathlib import Path import doeff_vm -from doeff import Gather, Pass, Resume, Spawn, WithHandler, do from doeff_core_effects.effects import EffectBase + +from doeff import Gather, Pass, Resume, Spawn, WithHandler, do +from doeff import run as vm_run + Effect = EffectBase # REMOVED: from doeff_core_effects.cache_handlers import memo_rewriters, sqlite_cache_handler # REMOVED: from doeff_vm import default_handlers -from doeff import run as vm_run def test_memory_stats_exported_with_expected_keys(): @@ -72,20 +74,19 @@ def scenario(): values = yield Gather(*tasks) if len(values) != 20: raise AssertionError(f"expected 20 values, got {len(values)}") - return None wrapped = scenario() for handler in reversed( ( synthetic_query_handler(), - *memo_rewriters(SyntheticQuery), - sqlite_cache_handler(cache_path), + *memo_rewriters(SyntheticQuery), # noqa: F821 - legacy removed API reference is intentionally preserved + sqlite_cache_handler(cache_path), # noqa: F821 - legacy removed API reference is intentionally preserved ) ): wrapped = WithHandler(handler, wrapped) before = doeff_vm.memory_stats() - result = vm_run(wrapped, handlers=default_handlers()) + result = vm_run(wrapped, handlers=default_handlers()) # noqa: F821 - legacy removed API reference is intentionally preserved after = doeff_vm.memory_stats() assert result.is_ok() @@ -128,13 +129,12 @@ def scenario(): values = yield Gather(*tasks) if len(values) != 20: raise AssertionError(f"expected 20 values, got {len(values)}") - return None program = scenario() for handler in reversed( ( synthetic_query_handler(), - *default_handlers(), + *default_handlers(), # noqa: F821 - legacy removed API reference is intentionally preserved ) ): program = WithHandler(handler, program) diff --git a/packages/doeff-vm/tests/test_pyvm.py b/packages/doeff-vm/tests/test_pyvm.py index d845a1ca8..caee4f662 100644 --- a/packages/doeff-vm/tests/test_pyvm.py +++ b/packages/doeff-vm/tests/test_pyvm.py @@ -3,8 +3,7 @@ import doeff_vm import pytest -from doeff import Effect, Program, do -from doeff import Ask, Get, Modify, Pure, Put, Tell +from doeff import Ask, Effect, Get, Modify, Program, Pure, Put, Tell, do def _with_handlers(program, *handlers): @@ -859,7 +858,7 @@ def test_run_scoped_handlers_do_not_leak(): assert result == 1 # Second run without handlers should fail (no state handler) - with pytest.raises(Exception, match="(no matching handler|unhandled effect)"): + with pytest.raises(Exception, match=r"(no matching handler|unhandled effect)"): vm.run(counter_program()) @@ -874,7 +873,7 @@ def test_run_scoped_handlers_do_not_leak_writer(): assert result == "completed" # Second run without handlers should fail - with pytest.raises(Exception, match="(no matching handler|unhandled effect)"): + with pytest.raises(Exception, match=r"(no matching handler|unhandled effect)"): vm.run(logging_program()) @@ -891,11 +890,11 @@ def failing_program() -> Program[int]: yield # make it a generator # This should raise but still clean up the handlers - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="intentional error"): vm.run(_with_handlers(failing_program(), doeff_vm.state)) # Verify handler was cleaned up: running a state program should fail - with pytest.raises(Exception, match="(no matching handler|unhandled effect)"): + with pytest.raises(Exception, match=r"(no matching handler|unhandled effect)"): vm.run(counter_program()) @@ -949,7 +948,7 @@ def main() -> Program[int]: result = yield sub_generator() return result - with pytest.raises(TypeError, match="(Expected ProgramBase|DoExpr)"): + with pytest.raises(TypeError, match=r"(Expected ProgramBase|DoExpr)"): vm.run(main()) @@ -1507,14 +1506,12 @@ def test_scheduler_effects_type_names_are_recognized(self): # contains explicit classify_yielded arms for all scheduler effects. # We import from the test to verify the effect classes exist and have # the expected type names that classify_yielded should match. - from doeff_core_effects.scheduler import Gather from doeff.effects.promise import CompletePromiseEffect, FailPromiseEffect from doeff.effects.race import RaceEffect - from doeff_core_effects.scheduler import Spawn # Verify the type names that classify_yielded needs to match - assert SpawnEffect.__name__ == "SpawnEffect" - assert GatherEffect.__name__ == "GatherEffect" + assert SpawnEffect.__name__ == "SpawnEffect" # noqa: F821 - legacy removed API reference is intentionally preserved + assert GatherEffect.__name__ == "GatherEffect" # noqa: F821 - legacy removed API reference is intentionally preserved assert RaceEffect.__name__ == "RaceEffect" assert CompletePromiseEffect.__name__ == "CompletePromiseEffect" assert FailPromiseEffect.__name__ == "FailPromiseEffect" @@ -1555,6 +1552,7 @@ def prog(): capture_output=True, text=True, timeout=3, + check=False, ) assert result.returncode == 0, f"Process crashed: {result.stderr}" assert result.stdout.strip() == "OK", ( @@ -1569,15 +1567,13 @@ def test_classify_yielded_must_check_effects_before_to_generator(self): so scheduler effect classes must not expose program-only conversion methods such as ``to_generator``. """ - from doeff_core_effects.scheduler import Gather from doeff.effects.promise import CompletePromiseEffect, FailPromiseEffect from doeff.effects.race import RaceEffect - from doeff_core_effects.scheduler import Spawn for cls in [ - GatherEffect, + GatherEffect, # noqa: F821 - legacy removed API reference is intentionally preserved RaceEffect, - SpawnEffect, + SpawnEffect, # noqa: F821 - legacy removed API reference is intentionally preserved CompletePromiseEffect, FailPromiseEffect, ]: @@ -1893,8 +1889,6 @@ def test_scheduler_spawn_creates_task(): """ISSUE-VM-003: SpawnEffect should create a task via the scheduler handler.""" from doeff_vm import PyVM - from doeff_core_effects.scheduler import Spawn - vm = PyVM() @do @@ -1904,7 +1898,7 @@ def child() -> Program[int]: @do def body() -> Program[dict]: - task = yield SpawnEffect(program=child(), handlers=[]) + task = yield SpawnEffect(program=child(), handlers=[]) # noqa: F821 - legacy removed API reference is intentionally preserved return task result = vm.run(_with_handlers(body(), doeff_vm.scheduler)) @@ -1923,14 +1917,12 @@ def test_scheduler_gather_recognized_by_handler(): """ from doeff_vm import PyVM - from doeff_core_effects.scheduler import Gather - vm = PyVM() @do def body() -> Program[object]: # GatherEffect with empty items — scheduler should handle it - result = yield GatherEffect(items=[]) + result = yield GatherEffect(items=[]) # noqa: F821 - legacy removed API reference is intentionally preserved return result result = vm.run_with_result(_with_handlers(body(), doeff_vm.scheduler)) @@ -1949,9 +1941,8 @@ def test_scheduler_race_recognized_by_handler(): Verifies that RaceEffect is classified correctly and dispatched to the scheduler handler (not rejected as UnhandledEffect or TypeError). """ - from doeff_vm import PyVM - from doeff.effects.race import RaceEffect + from doeff_vm import PyVM vm = PyVM() @@ -2000,7 +1991,7 @@ def body() -> Program[int]: ) except TypeError as e: # Good — the VM rejected the non-DoExpr return - assert "flat_map" in str(e).lower() or "DoExpr" in str(e), ( + assert "flat_map" in str(e).lower() or "DoExpr" in str(e), ( # noqa: PT017 - legacy assertion style is kept unchanged for baseline cleanup f"TypeError raised but message doesn't mention flat_map/DoExpr: {e}" ) diff --git a/packages/doeff-vm/tests/test_traceback_pure_python_frames.py b/packages/doeff-vm/tests/test_traceback_pure_python_frames.py index 69d3cf256..8831c9965 100644 --- a/packages/doeff-vm/tests/test_traceback_pure_python_frames.py +++ b/packages/doeff-vm/tests/test_traceback_pure_python_frames.py @@ -29,10 +29,9 @@ """ import pytest - -from doeff import Program, do, run from doeff_vm import WithHandler +from doeff import Program, do, run # --- Pure Python function that raises --- @@ -85,7 +84,7 @@ def test_pure_python_frames_in_traceback(): run(program) exc = exc_info.value - tb_data = getattr(exc, '__doeff_traceback__', None) + tb_data = getattr(exc, "__doeff_traceback__", None) # The traceback data should exist assert tb_data is not None, ( @@ -122,7 +121,7 @@ def test_pure_python_frames_without_handler(): run(program_calling_pure_function()) exc = exc_info.value - tb_data = getattr(exc, '__doeff_traceback__', None) + tb_data = getattr(exc, "__doeff_traceback__", None) # Even without handlers, the pure Python frames should be captured if tb_data is not None: @@ -165,7 +164,7 @@ def handler_b(effect, k): run(program) exc = exc_info.value - tb_data = getattr(exc, '__doeff_traceback__', None) + tb_data = getattr(exc, "__doeff_traceback__", None) assert tb_data is not None frame_names = [] From 12504e589d3491d3758f76364b3242f582c61653 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:11:59 +0900 Subject: [PATCH 27/35] Clean Ruff violations in doeff-vm-core --- .../tests/test_dispatch_id_elimination_architecture.py | 1 - .../doeff-vm-core/tests/test_dispatch_origin_architecture.py | 1 - .../tests/test_exception_enrichment_architecture.py | 4 ++-- .../tests/test_frame_based_traceback_architecture.py | 1 - .../tests/test_interceptor_chain_architecture.py | 1 - .../doeff-vm-core/tests/test_lexical_scope_architecture.py | 4 ++-- .../doeff-vm-core/tests/test_move_semantics_architecture.py | 1 - packages/doeff-vm-core/tests/test_vm_module_split.py | 1 - 8 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/doeff-vm-core/tests/test_dispatch_id_elimination_architecture.py b/packages/doeff-vm-core/tests/test_dispatch_id_elimination_architecture.py index 80ecf30e3..0509bbf09 100644 --- a/packages/doeff-vm-core/tests/test_dispatch_id_elimination_architecture.py +++ b/packages/doeff-vm-core/tests/test_dispatch_id_elimination_architecture.py @@ -1,6 +1,5 @@ from pathlib import Path - CORE_ROOT = Path(__file__).resolve().parents[1] IDS_RS = CORE_ROOT / "src/ids.rs" FRAME_RS = CORE_ROOT / "src/frame.rs" diff --git a/packages/doeff-vm-core/tests/test_dispatch_origin_architecture.py b/packages/doeff-vm-core/tests/test_dispatch_origin_architecture.py index 9e9fde1fe..700fa615d 100644 --- a/packages/doeff-vm-core/tests/test_dispatch_origin_architecture.py +++ b/packages/doeff-vm-core/tests/test_dispatch_origin_architecture.py @@ -1,6 +1,5 @@ from pathlib import Path - CORE_ROOT = Path(__file__).resolve().parents[1] FRAME_RS = CORE_ROOT / "src/frame.rs" SEGMENT_RS = CORE_ROOT / "src/segment.rs" diff --git a/packages/doeff-vm-core/tests/test_exception_enrichment_architecture.py b/packages/doeff-vm-core/tests/test_exception_enrichment_architecture.py index 2f2321176..31fd8c896 100644 --- a/packages/doeff-vm-core/tests/test_exception_enrichment_architecture.py +++ b/packages/doeff-vm-core/tests/test_exception_enrichment_architecture.py @@ -1,6 +1,5 @@ from pathlib import Path - CORE_ROOT = Path(__file__).resolve().parents[1] VM_TRACE_RS = CORE_ROOT / "src/vm/vm_trace.rs" VM_STEP_RS = CORE_ROOT / "src/vm/step.rs" @@ -27,7 +26,8 @@ def test_exception_enrichment_callers_assemble_filtered_active_chain() -> None: dispatch_source = _runtime_source(VM_DISPATCH_RS) for source in (step_source, dispatch_source): - assert "let active_chain = self" in source and ".assemble_active_chain(Some(&" in source, ( + assert "let active_chain = self" in source + assert ".assemble_active_chain(Some(&" in source, ( "Exception enrichment call sites must assemble the active chain explicitly before " "calling TraceState::enrich_original_exception_with_context." ) diff --git a/packages/doeff-vm-core/tests/test_frame_based_traceback_architecture.py b/packages/doeff-vm-core/tests/test_frame_based_traceback_architecture.py index a6eedbe1b..e6038a4ef 100644 --- a/packages/doeff-vm-core/tests/test_frame_based_traceback_architecture.py +++ b/packages/doeff-vm-core/tests/test_frame_based_traceback_architecture.py @@ -1,6 +1,5 @@ from pathlib import Path - CORE_ROOT = Path(__file__).resolve().parents[1] TRACE_STATE_RS = CORE_ROOT / "src/trace_state.rs" CAPTURE_RS = CORE_ROOT / "src/capture.rs" diff --git a/packages/doeff-vm-core/tests/test_interceptor_chain_architecture.py b/packages/doeff-vm-core/tests/test_interceptor_chain_architecture.py index 424481418..175a56d79 100644 --- a/packages/doeff-vm-core/tests/test_interceptor_chain_architecture.py +++ b/packages/doeff-vm-core/tests/test_interceptor_chain_architecture.py @@ -2,7 +2,6 @@ from pathlib import Path - ROOT = Path(__file__).resolve().parents[3] VM_STEP_RS = ROOT / "packages" / "doeff-vm-core" / "src" / "vm" / "step.rs" diff --git a/packages/doeff-vm-core/tests/test_lexical_scope_architecture.py b/packages/doeff-vm-core/tests/test_lexical_scope_architecture.py index b19deaa43..9fccb86ca 100644 --- a/packages/doeff-vm-core/tests/test_lexical_scope_architecture.py +++ b/packages/doeff-vm-core/tests/test_lexical_scope_architecture.py @@ -2,7 +2,6 @@ from pathlib import Path - CORE_ROOT = Path(__file__).resolve().parents[1] SEGMENT_RS = CORE_ROOT / "src" / "segment.rs" VAR_STORE_RS = CORE_ROOT / "src" / "var_store.rs" @@ -76,7 +75,8 @@ def test_scheduler_spawn_path_no_longer_requests_get_handlers() -> None: start = source.find("SchedulerPhase::SpawnAwaitTraceback") end = source.find("SchedulerPhase::SpawnAwaitContinuation") - assert start != -1 and end != -1, "scheduler spawn phases must exist" + assert start != -1, "scheduler spawn start phase must exist" + assert end != -1, "scheduler spawn continuation phase must exist" spawn_block = source[start:end] assert "DoCtrl::GetHandlers" not in spawn_block, ( diff --git a/packages/doeff-vm-core/tests/test_move_semantics_architecture.py b/packages/doeff-vm-core/tests/test_move_semantics_architecture.py index 085adc573..8f197beba 100644 --- a/packages/doeff-vm-core/tests/test_move_semantics_architecture.py +++ b/packages/doeff-vm-core/tests/test_move_semantics_architecture.py @@ -1,6 +1,5 @@ from pathlib import Path - CORE_ROOT = Path(__file__).resolve().parents[1] CONTINUATION_RS = CORE_ROOT / "src/continuation.rs" VM_DISPATCH_RS = CORE_ROOT / "src/vm/dispatch.rs" diff --git a/packages/doeff-vm-core/tests/test_vm_module_split.py b/packages/doeff-vm-core/tests/test_vm_module_split.py index 320d57381..bf278f1a7 100644 --- a/packages/doeff-vm-core/tests/test_vm_module_split.py +++ b/packages/doeff-vm-core/tests/test_vm_module_split.py @@ -1,6 +1,5 @@ from pathlib import Path - CORE_ROOT = Path(__file__).resolve().parents[1] VM_RS = CORE_ROOT / "src" / "vm.rs" VM_STEP_RS = CORE_ROOT / "src" / "vm" / "step.rs" From 5b50bed75f931d74899d3c97120a76a9f7b20ada Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:15:32 +0900 Subject: [PATCH 28/35] Document Ruff noqa reasons in core tests --- tests/public_api/test_do_bang_defp_defpp.py | 4 ++-- tests/test_do_bang_setv.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/public_api/test_do_bang_defp_defpp.py b/tests/public_api/test_do_bang_defp_defpp.py index fc118f285..6c0dfda1f 100644 --- a/tests/public_api/test_do_bang_defp_defpp.py +++ b/tests/public_api/test_do_bang_defp_defpp.py @@ -179,8 +179,8 @@ def _import_repro(self) -> None: import os import sys - import doeff_hy # noqa: F401 - import hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macro/import hooks for fixture import + import hy # noqa: F401 - activates Hy reader for fixture import sys.path.insert(0, os.path.join(os.getcwd(), "tests")) sys.modules.pop("fixtures.repro_388", None) diff --git a/tests/test_do_bang_setv.py b/tests/test_do_bang_setv.py index 06c4303a5..9a926233f 100644 --- a/tests/test_do_bang_setv.py +++ b/tests/test_do_bang_setv.py @@ -5,7 +5,7 @@ must be accessible after yield. """ -import hy # noqa: F401 +import hy # noqa: F401 - activates Hy import hooks for macro tests from doeff_core_effects import slog from doeff_core_effects.handlers import lazy_ask, writer from doeff_core_effects.scheduler import scheduled From 95cd3d9d2960c77140004f55126d541cf2ec8235 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:15:37 +0900 Subject: [PATCH 29/35] Document Ruff noqa reasons in doeff-hy --- .../doeff-hy/tests/test_lazy_and_threading.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/doeff-hy/tests/test_lazy_and_threading.py b/packages/doeff-hy/tests/test_lazy_and_threading.py index 8649cc8a5..4ee8f4c86 100644 --- a/packages/doeff-hy/tests/test_lazy_and_threading.py +++ b/packages/doeff-hy/tests/test_lazy_and_threading.py @@ -52,7 +52,7 @@ class TestLazyHandlerMacro: def test_lazy_handler_basic(self): """Lazy init runs on first effect, uses cached value on second.""" - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy # This Hy code uses the lazy clause in defhandler @@ -95,8 +95,8 @@ def test_lazy_handler_basic(self): def test_lazy_handler_runs_once(self): """Lazy init should execute only once even with multiple effect invocations.""" - import doeff_hy # noqa: F401 - import hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval + import hy # noqa: F401 - activates Hy reader for inline eval # We test by checking state key contains Some after first init @do @@ -169,7 +169,7 @@ class TestLazyHandlerHyIntegration: def test_defhandler_lazy_compiles(self): """(defhandler name (lazy x ...) (Effect [f] ...)) should compile.""" - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy # This should NOT raise a SyntaxError once implemented @@ -188,7 +188,7 @@ def test_defhandler_lazy_compiles(self): def test_defhandler_lazy_with_effects(self): """Lazy init body can perform effects (Ask) for config.""" - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy code = """ @@ -237,7 +237,7 @@ def test_defhandler_lazy_key_contains_module(self): """Lazy state key should be prefixed with module name.""" import types - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy observed_keys = [] @@ -307,7 +307,7 @@ def test_defk_lazy_basic(self): """(defk name [x] (lazy val ...) body) should compile and run.""" import types - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy code = """ @@ -357,7 +357,7 @@ def test_threading_expands_correctly(self): """(<-> (f) (g :k v) (h)) should thread first-arg through pipeline.""" import types - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy code = """ @@ -407,7 +407,7 @@ def test_threading_single_form(self): """(<-> (f x)) with one form is just (<- _t0 (f x)), _t0.""" import types - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy code = """ @@ -482,7 +482,7 @@ def _hy_eval_in_module(code, mod_name): import sys import types - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy mod = types.ModuleType(mod_name) @@ -612,7 +612,7 @@ def test_lazy_var_set_bang_persists_across_calls(self): def test_lazy_val_set_bang_compile_error(self): """set! on lazy-val should raise SyntaxError at compile time.""" - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy code = """ From 7720e6f4743007ad89fec2afff759b6623aedef9 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:15:41 +0900 Subject: [PATCH 30/35] Document Ruff noqa reasons in doeff-gemini --- packages/doeff-gemini/tests/e2e/test_gemini_live.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/doeff-gemini/tests/e2e/test_gemini_live.py b/packages/doeff-gemini/tests/e2e/test_gemini_live.py index 4b4668c20..51a2a593e 100644 --- a/packages/doeff-gemini/tests/e2e/test_gemini_live.py +++ b/packages/doeff-gemini/tests/e2e/test_gemini_live.py @@ -21,7 +21,7 @@ if str(PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(PACKAGE_ROOT)) -from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 +from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 - late import follows sys.path fixture setup from doeff_gemini import edit_image__gemini, structured_llm__gemini # noqa: E402 from doeff_gemini.handlers import default_gemini_cost_handler # noqa: E402 From dceed272abb1cfcc3ab1f48a7829307f2a0ca4f7 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:15:45 +0900 Subject: [PATCH 31/35] Document Ruff noqa reasons in doeff-google-secret-manager --- .../doeff-google-secret-manager/tests/unit/test_secrets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 429393bed..3afd0e8f4 100644 --- a/packages/doeff-google-secret-manager/tests/unit/test_secrets.py +++ b/packages/doeff-google-secret-manager/tests/unit/test_secrets.py @@ -14,14 +14,14 @@ if str(SECRET_PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(SECRET_PACKAGE_ROOT)) -from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 +from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 - late import follows sys.path fixture setup from doeff_google_secret_manager import ( # noqa: E402 SecretManagerClient, access_secret, get_secret_manager_client, ) -from doeff import ( # noqa: E402 - late import preserves existing import/setup order # noqa: E402 +from doeff import ( # noqa: E402 - late import preserves existing import/setup order AskEffect, Pass, Resume, @@ -30,7 +30,7 @@ do, run, ) -from doeff import Get as StateGetEffect # noqa: E402 +from doeff import Get as StateGetEffect # noqa: E402 - late import follows sys.path fixture setup from doeff import ( # noqa: E402 - late import preserves existing import/setup order Put as StatePutEffect, ) From e22f32178a7194435327fc81945372d95b317631 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:15:48 +0900 Subject: [PATCH 32/35] Document Ruff noqa reasons in doeff-ml-nexus --- packages/doeff-ml-nexus/tests/test_docker.py | 2 +- packages/doeff-ml-nexus/tests/test_uv_deps.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/doeff-ml-nexus/tests/test_docker.py b/packages/doeff-ml-nexus/tests/test_docker.py index c6bb2f6e7..593f3ae64 100644 --- a/packages/doeff-ml-nexus/tests/test_docker.py +++ b/packages/doeff-ml-nexus/tests/test_docker.py @@ -1,6 +1,6 @@ """Tests for uv-image Dockerfile generation.""" -import hy # noqa: F401 +import hy # noqa: F401 - activates Hy import hooks for test modules from doeff import run, WithHandler, do from doeff_core_effects import reader, writer, slog_handler diff --git a/packages/doeff-ml-nexus/tests/test_uv_deps.py b/packages/doeff-ml-nexus/tests/test_uv_deps.py index 153b72438..f358897eb 100644 --- a/packages/doeff-ml-nexus/tests/test_uv_deps.py +++ b/packages/doeff-ml-nexus/tests/test_uv_deps.py @@ -1,6 +1,6 @@ """Tests for uv dependency detection and rewriting.""" -import hy # noqa: F401 +import hy # noqa: F401 - activates Hy import hooks for test modules from pathlib import Path from doeff_ml_nexus.uv_deps import ( From 5867ecf101d748c465855342f23c805a98e22745 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:16:10 +0900 Subject: [PATCH 33/35] Document remaining Ruff noqa reasons in doeff-hy --- packages/doeff-hy/tests/test_lazy_and_threading.py | 2 +- packages/doeff-hy/tests/test_tail_resume_tco.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/doeff-hy/tests/test_lazy_and_threading.py b/packages/doeff-hy/tests/test_lazy_and_threading.py index 4ee8f4c86..2784eadb4 100644 --- a/packages/doeff-hy/tests/test_lazy_and_threading.py +++ b/packages/doeff-hy/tests/test_lazy_and_threading.py @@ -439,7 +439,7 @@ def test_threading_two_forms(self): """(<-> (f) (g)) threads result of f as first arg to g.""" import types - import doeff_hy # noqa: F401 + import doeff_hy # noqa: F401 - registers Hy macros/import hooks for inline eval import hy code = """ diff --git a/packages/doeff-hy/tests/test_tail_resume_tco.py b/packages/doeff-hy/tests/test_tail_resume_tco.py index 74bfa4dd4..c211f0aef 100644 --- a/packages/doeff-hy/tests/test_tail_resume_tco.py +++ b/packages/doeff-hy/tests/test_tail_resume_tco.py @@ -9,7 +9,7 @@ """ import doeff_hy # noqa: F401 — registers macros/extensions -import hy # noqa: F401 +import hy # noqa: F401 - activates Hy reader for macro tests from doeff_hy.handle import _build_clause from hy.models import Expression, Symbol from hy.models import List as HyList From 51afe9450ca1be206db2bd461330e9f45441e80b Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:16:57 +0900 Subject: [PATCH 34/35] Format Ruff noqa import in doeff-gemini --- packages/doeff-gemini/tests/e2e/test_gemini_live.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/doeff-gemini/tests/e2e/test_gemini_live.py b/packages/doeff-gemini/tests/e2e/test_gemini_live.py index 51a2a593e..351a25bdb 100644 --- a/packages/doeff-gemini/tests/e2e/test_gemini_live.py +++ b/packages/doeff-gemini/tests/e2e/test_gemini_live.py @@ -21,7 +21,9 @@ if str(PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(PACKAGE_ROOT)) -from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 - late import follows sys.path fixture setup +from doeff_core_effects.effects import ( # noqa: E402 - late import follows sys.path fixture setup + EffectBase as Effect, +) from doeff_gemini import edit_image__gemini, structured_llm__gemini # noqa: E402 from doeff_gemini.handlers import default_gemini_cost_handler # noqa: E402 From 852bc45a14233b0d8a86beb3b2976cd2e7a7b5a8 Mon Sep 17 00:00:00 2001 From: proboscis Date: Thu, 11 Jun 2026 20:17:00 +0900 Subject: [PATCH 35/35] Format Ruff noqa import in doeff-google-secret-manager --- .../doeff-google-secret-manager/tests/unit/test_secrets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 3afd0e8f4..a315e5723 100644 --- a/packages/doeff-google-secret-manager/tests/unit/test_secrets.py +++ b/packages/doeff-google-secret-manager/tests/unit/test_secrets.py @@ -14,7 +14,9 @@ if str(SECRET_PACKAGE_ROOT) not in sys.path: sys.path.insert(0, str(SECRET_PACKAGE_ROOT)) -from doeff_core_effects.effects import EffectBase as Effect # noqa: E402 - late import follows sys.path fixture setup +from doeff_core_effects.effects import ( # noqa: E402 - late import follows sys.path fixture setup + EffectBase as Effect, +) from doeff_google_secret_manager import ( # noqa: E402 SecretManagerClient, access_secret,