from unittest.mock import Mock
from typing import Callable
from .exception import AllowsException
from .side_effect import SideEffectBuilder
[docs]class MockExtensionGrammar:
"""
MockExtensionGrammar is created by the ``allow`` factory.
This enables grammar for creating and binding a mock side effect like:
*allow* **<Mock>** *to* **<Have Side Effect>**
"""
def __init__(self, mock_subject: Mock) -> None:
self._mock_subject = mock_subject
def to(self, side_effect_builder_grammar: "SideEffectBuilderGrammar") -> Mock:
return side_effect_builder_grammar.apply_to(self._mock_subject)
[docs]class SideEffectBuilderGrammar:
"""
SideEffectBuilderGrammar is initiated by the ``return_value``, ``raise_exception``,
``receive_method``, ``be_called_with``, ``have_effect`` factory methods.
The grammar is chainable, but a side effect can have only one effect (return,
exception, effect) per grammar expression. However, side effects will automatically
combine if multiple expressions are applied to the same mock/method.
This enables grammar for building the side effect like:
*allow* **<Mock>** *to* ...
*be_called_with* **<Args>** *on_method* **<Name>** *and_return_value* **<Value>**
"""
def __init__(self, method_name=None, builder=None):
self._method_name = method_name
self._builder = builder or SideEffectBuilder()
def _set_method_name(self, method_name: str) -> "SideEffectBuilderGrammar":
if self._method_name:
raise AllowsException("Cannot set mutliple method names in one effect")
self._method_name = method_name
return self
[docs] def on_method(self, method_name):
"""
Specify method name on the mocked object which will have the side effect
applied.
allow(my_mock).to(return_value(5).on_method('foo'))
assert my_mock.foo() == 5
"""
self._set_method_name(method_name)
return self
[docs] def called_with(self, *args, **kwargs):
"""
Specify call args that trigger the side effect. Alias ``when_called_with``.
"""
self._builder.with_call_args(*args, **kwargs)
return self
def when_called_with(self, *args, **kwargs):
self._builder.with_call_args(*args, **kwargs)
return self
def and_return(self, *return_value):
self._builder.with_return_value(*return_value)
return self
[docs] def and_return_value(self, *return_value):
"""
Add a return value (or a list of return values to cycle through). Alias
``and_return``.
"""
self._builder.with_return_value(*return_value)
return self
def and_raise(self, raised_exception):
self._builder.with_raised_exception(raised_exception)
return self
[docs] def and_raise_exception(self, raised_exception):
"""
Raise an exception. Alias ``and_raise``.
"""
self._builder.with_raised_exception(raised_exception)
return self
[docs] def with_effect(self, effect: Callable):
"""
Apply a generic effect when invoking the side effect.
"""
self._builder.with_effect(effect)
return self
[docs] def apply_to(self, mock_subject: Mock) -> Mock:
"""
Apply the built side effect to the given Python Mock.
"""
mock_method = self._get_method_from_mock(mock_subject)
side_effect = self._builder.build()
mock_method.side_effect = side_effect.merge(mock_method.side_effect)
return mock_subject
def _get_method_from_mock(self, mock_subject):
if not self._method_name:
return mock_subject
method_path = self._method_name.split(".")
for method in method_path:
mock_subject = getattr(mock_subject, method)
return mock_subject