2024-06-30 21:02:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
import unittest.mock
|
2024-07-13 15:55:58 +00:00
|
|
|
from typing import Any, ClassVar, Generic, TYPE_CHECKING, TypeVar
|
2024-06-30 21:02:19 +00:00
|
|
|
|
|
|
|
from typing_extensions import ParamSpec
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
from collections.abc import Callable, Coroutine
|
|
|
|
|
|
|
|
T = TypeVar("T")
|
|
|
|
P = ParamSpec("P")
|
|
|
|
T_Mock = TypeVar("T_Mock", bound=unittest.mock.Mock)
|
|
|
|
|
|
|
|
__all__ = [
|
|
|
|
"synchronize",
|
|
|
|
"UnpropagatingMockMixin",
|
|
|
|
"CustomMockMixin",
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def synchronize(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
|
|
|
|
"""Take an asynchronous function, and return a synchronous alternative.
|
|
|
|
|
|
|
|
This is needed because we sometimes want to test asynchronous behavior in a synchronous test function,
|
|
|
|
where we can't simply await something. This function uses `asyncio.run` and generates a wrapper
|
|
|
|
around the original asynchronous function, that awaits the result in a blocking synchronous way,
|
|
|
|
returning the obtained value.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
|
|
return asyncio.run(f(*args, **kwargs))
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
class UnpropagatingMockMixin(Generic[T_Mock]):
|
|
|
|
"""Provides common functionality for our :class:`~unittest.mock.Mock` classes.
|
|
|
|
|
|
|
|
By default, mock objects propagate themselves by returning a new instance of the same mock
|
|
|
|
class, with same initialization attributes. This is done whenever we're accessing new
|
|
|
|
attributes that mock class.
|
|
|
|
|
|
|
|
This propagation makes sense for simple mocks without any additional restrictions, however when
|
|
|
|
dealing with limited mocks to some ``spec_set``, it doesn't usually make sense to propagate
|
|
|
|
those same ``spec_set`` restrictions, since we generally don't have attributes/methods of a
|
|
|
|
class be of/return the same class.
|
|
|
|
|
|
|
|
This mixin class stops this propagation, and instead returns instances of specified mock class,
|
|
|
|
defined in :attr:`.child_mock_type` class variable, which is by default set to
|
|
|
|
:class:`~unittest.mock.MagicMock`, as it can safely represent most objects.
|
|
|
|
|
|
|
|
.. note:
|
|
|
|
This propagation handling will only be done for the mock classes that inherited from this
|
|
|
|
mixin class. That means if the :attr:`.child_mock_type` is one of the regular mock classes,
|
|
|
|
and the mock is propagated, a regular mock class is returned as that new attribute. This
|
|
|
|
regular class then won't have the same overrides, and will therefore propagate itself, like
|
|
|
|
any other mock class would.
|
|
|
|
|
|
|
|
If you wish to counteract this, you can set the :attr:`.child_mock_type` to a mock class
|
|
|
|
that also inherits from this mixin class, perhaps to your class itself, overriding any
|
|
|
|
propagation recursively.
|
|
|
|
"""
|
|
|
|
|
|
|
|
child_mock_type: T_Mock = unittest.mock.MagicMock
|
|
|
|
|
|
|
|
# Since this is a mixin class, we can access some attributes defined in mock classes safely.
|
|
|
|
# Define the types of these variables here, for proper static type analysis.
|
|
|
|
_mock_sealed: bool
|
|
|
|
_extract_mock_name: Callable[[], str]
|
|
|
|
|
2024-07-13 11:42:52 +00:00
|
|
|
def _get_child_mock(self, **kwargs: object) -> T_Mock:
|
2024-06-30 21:02:19 +00:00
|
|
|
"""Make :attr:`.child_mock_type`` instances instead of instances of the same class.
|
|
|
|
|
|
|
|
By default, this method creates a new mock instance of the same original class, and passes
|
|
|
|
over the same initialization arguments. This overrides that behavior to instead create an
|
|
|
|
instance of :attr:`.child_mock_type` class.
|
|
|
|
"""
|
|
|
|
# Mocks can be sealed, in which case we wouldn't want to allow propagation of any kind
|
|
|
|
# and rather raise an AttributeError, informing that given attr isn't accessible
|
|
|
|
if self._mock_sealed:
|
|
|
|
mock_name = self._extract_mock_name()
|
|
|
|
obj_name = f"{mock_name}.{kwargs['name']}" if "name" in kwargs else f"{mock_name}()"
|
|
|
|
raise AttributeError(f"Can't access {obj_name}, mock is sealed.")
|
|
|
|
|
2024-07-13 15:55:58 +00:00
|
|
|
# Propagate any other children as the `child_mock_type` instances
|
2024-06-30 21:02:19 +00:00
|
|
|
# rather than `self.__class__` instances
|
|
|
|
return self.child_mock_type(**kwargs)
|
|
|
|
|
|
|
|
|
2024-07-13 11:42:52 +00:00
|
|
|
class CustomMockMixin(UnpropagatingMockMixin[T_Mock], Generic[T_Mock]):
|
2024-06-30 21:02:19 +00:00
|
|
|
"""Provides common functionality for our custom mock types.
|
|
|
|
|
|
|
|
* Stops propagation of same ``spec_set`` restricted mock in child mocks
|
|
|
|
(see :class:`.UnpropagatingMockMixin` for more info)
|
|
|
|
* Allows using the ``spec_set`` attribute as class attribute
|
|
|
|
"""
|
|
|
|
|
2024-07-13 15:55:58 +00:00
|
|
|
spec_set: ClassVar[object] = None
|
2024-06-30 21:02:19 +00:00
|
|
|
|
2024-07-13 11:42:52 +00:00
|
|
|
def __init__(self, **kwargs: object):
|
2024-07-13 15:55:58 +00:00
|
|
|
# If `spec_set` is explicitly passed, have it take precedence over the class attribute.
|
|
|
|
#
|
|
|
|
# Although this is an edge case, and there usually shouldn't be a need for this.
|
|
|
|
# This is mostly for the sake of completeness, and to allow for more flexibility.
|
2024-06-30 21:02:19 +00:00
|
|
|
if "spec_set" in kwargs:
|
2024-07-13 15:55:58 +00:00
|
|
|
spec_set = kwargs.pop("spec_set")
|
|
|
|
else:
|
|
|
|
spec_set = self.spec_set
|
|
|
|
|
|
|
|
super().__init__(spec_set=spec_set, **kwargs) # pyright: ignore[reportCallIssue] # Mixin class, this __init__ is valid
|