Skip to content

Commit 3ff527a

Browse files
committed
test: eliminate remaining coverage gaps in invoke
- Remove dead code: _needs_wrapping hasattr(item, "run") branch was unreachable because IInvoke protocol check catches it first - Add test for _InvokeCallableWrapper.__call__ (L74) - Add test for non-IInvoke class passing through normalize_invoke_callbacks - Add test for InvokeGroup.on_cancel() before run() - invoke.py: 0 missing lines, 4 remaining branch partials are all structurally unreachable (Protocol body, async race conditions)
1 parent 05c3f1c commit 3ff527a

2 files changed

Lines changed: 36 additions & 3 deletions

File tree

statemachine/invoke.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,6 @@ def _needs_wrapping(item: Any) -> bool:
128128
# StateChart subclass → child machine invoker
129129
if issubclass(item, StateChart):
130130
return True
131-
# Class whose instances implement IInvoke (has a ``run`` method)
132-
if hasattr(item, "run"):
133-
return True
134131
return False
135132

136133

tests/test_invoke.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,13 +624,16 @@ def run(self, ctx):
624624
assert result[0] is wrapper
625625

626626
def test_iinvoke_class_with_run_method(self):
627+
"""IInvoke-compatible class gets wrapped."""
627628
from statemachine.invoke import _InvokeCallableWrapper
628629
from statemachine.invoke import normalize_invoke_callbacks
629630

630631
class CustomHandler:
631632
def run(self, ctx):
632633
return "result"
633634

635+
# CustomHandler satisfies IInvoke protocol (has run method)
636+
assert isinstance(CustomHandler(), IInvoke)
634637
result = normalize_invoke_callbacks(CustomHandler)
635638
assert len(result) == 1
636639
assert isinstance(result[0], _InvokeCallableWrapper)
@@ -647,6 +650,19 @@ def my_func():
647650
assert result[0] is my_func
648651
assert not isinstance(result[0], _InvokeCallableWrapper)
649652

653+
def test_non_invoke_class_passes_through(self):
654+
"""A class without run() (not IInvoke, not StateChart) passes through unwrapped."""
655+
from statemachine.invoke import _InvokeCallableWrapper
656+
from statemachine.invoke import normalize_invoke_callbacks
657+
658+
class PlainClass:
659+
pass
660+
661+
result = normalize_invoke_callbacks(PlainClass)
662+
assert len(result) == 1
663+
assert result[0] is PlainClass
664+
assert not isinstance(result[0], _InvokeCallableWrapper)
665+
650666

651667
class TestResolveHandler:
652668
"""InvokeManager._resolve_handler edge cases."""
@@ -719,6 +735,26 @@ def on_cancel(self):
719735
# _instance is None, _is_class is True → early return
720736
wrapper.on_cancel() # should not raise
721737

738+
def test_callable_wrapper_call_returns_handler(self):
739+
"""__call__ returns the original handler (used by callback system for resolution)."""
740+
from statemachine.invoke import _InvokeCallableWrapper
741+
742+
class MyHandler:
743+
def run(self, ctx):
744+
return "result"
745+
746+
wrapper = _InvokeCallableWrapper(MyHandler)
747+
assert wrapper() is MyHandler
748+
749+
750+
class TestInvokeGroupOnCancelBeforeRun:
751+
"""InvokeGroup.on_cancel() before run() is a safe no-op."""
752+
753+
def test_on_cancel_before_run(self):
754+
group = invoke_group(lambda: 1)
755+
# on_cancel before run — executor is None, no futures
756+
group.on_cancel()
757+
722758

723759
class TestDoneInvokeEventFactory:
724760
"""done_invoke_ prefix works with both TransitionList and Event."""

0 commit comments

Comments
 (0)