Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incompatible with spy wrapper from pytest-mock #391

Closed
wawa19933 opened this issue Jun 12, 2023 · 3 comments · Fixed by #392
Closed

Incompatible with spy wrapper from pytest-mock #391

wawa19933 opened this issue Jun 12, 2023 · 3 comments · Fixed by #392
Labels

Comments

@wawa19933
Copy link

  • Python State Machine version: 2.0.0+ (2.0.0, 2.1.0)
  • Python version: 3.10+
  • Operating System: Linux

Description

Throws exception during construction, when using pytest-mock.

from traceback:

cls = <class 'statemachine.signature.SignatureAdapter'>
method = <function on_enter_state at 0x7fa0ca393740>

    @classmethod
    def wrap(cls, method):
        """Build a wrapper that adapts the received arguments to the inner ``method`` signature"""
    
        sig = cls.from_callable(method)
>       sig.method = method
E       AttributeError: 'Signature' object has no attribute 'method'

.venv/lib/python3.11/site-packages/statemachine/signature.py:18: AttributeError

Issue is caused by mock, that replaces class method with a wrapper, which already contains Signature instance. This instance is returned from cls.from_callable instead of SignatureAdapter.

What I Did

here is a minimal code example

def test_minimal(mocker):
    class Observer:
        def on_enter_state(self, event, model, source, target, state):
            ...

    obs = Observer()
    on_enter_state = mocker.spy(obs, "on_enter_state")

    class Machine(StateMachine):
        a = State("Init", initial=True)
        b = State("Fin")

        cycle = a.to(b) | b.to(a)

    state = Machine().add_observer(obs)
    assert state.a.is_active

    state.cycle()

    assert state.b.is_active
    on_enter_state.assert_called_once()
@wawa19933
Copy link
Author

Here is a dirty workaround:

--- a/signature.py  2023-06-12 04:14:29.750363476 +0200
+++ b/signature.py  2023-06-12 04:14:43.009982670 +0200
@@ -15,6 +15,14 @@
         """Build a wrapper that adapts the received arguments to the inner ``method`` signature"""

         sig = cls.from_callable(method)
+        if not isinstance(sig, SignatureAdapter):
+            n = SignatureAdapter()
+            n._parameters = sig._parameters
+            n.__slots__ = sig.__slots__
+            n._parameter_cls = sig._parameter_cls
+            n._bound_arguments_cls = sig._bound_arguments_cls
+            n.empty = sig.empty
+            sig = n
         sig.method = method
         sig.__name__ = (
             method.func.__name__ if isinstance(method, partial) else method.__name__

@wawa19933
Copy link
Author

wawa19933 commented Jun 12, 2023

This doesn't affect underlying unittest.mock patch functions, though.

So, this example works:

    class Observer:
        def on_enter_state(self, event, model, source, target, state):
            ...

    obs = Observer()
    # on_enter_state = mocker.spy(obs, "on_enter_state")
    on_enter_state = mocker.patch.object(obs, "on_enter_state")

    class Machine(StateMachine):
        a = State("Init", initial=True)
        b = State("Fin")

        cycle = a.to(b) | b.to(a)

        def on_exit_state(self, source, target):
            assert source != target

    state = Machine().add_observer(obs)
    assert state.a.is_active

    state.cycle()

    assert state.b.is_active
    on_enter_state.assert_called_once()

@fgmacedo
Copy link
Owner

fgmacedo commented Jun 12, 2023

Hi @wawa19933 , thanks for reporting this and the extra care on the context and details. I've added pytest-mock as a dependency to the project to be able to reproduce.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants