Compare commits
3 commits
9922734472
...
43fe0312ec
Author | SHA1 | Date | |
---|---|---|---|
ItsDrike | 43fe0312ec | ||
ItsDrike | 53a4583f9e | ||
ItsDrike | 354fbd26f7 |
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
from typing import Any, Generic, TYPE_CHECKING, TypeVar
|
from typing import Any, ClassVar, Generic, TYPE_CHECKING, TypeVar
|
||||||
|
|
||||||
from typing_extensions import ParamSpec
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ class UnpropagatingMockMixin(Generic[T_Mock]):
|
||||||
_mock_sealed: bool
|
_mock_sealed: bool
|
||||||
_extract_mock_name: Callable[[], str]
|
_extract_mock_name: Callable[[], str]
|
||||||
|
|
||||||
def _get_child_mock(self, **kwargs) -> T_Mock:
|
def _get_child_mock(self, **kwargs: object) -> T_Mock:
|
||||||
"""Make :attr:`.child_mock_type`` instances instead of instances of the same class.
|
"""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
|
By default, this method creates a new mock instance of the same original class, and passes
|
||||||
|
@ -84,12 +84,12 @@ class UnpropagatingMockMixin(Generic[T_Mock]):
|
||||||
obj_name = f"{mock_name}.{kwargs['name']}" if "name" in kwargs else f"{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.")
|
raise AttributeError(f"Can't access {obj_name}, mock is sealed.")
|
||||||
|
|
||||||
# Propagate any other children as simple `unittest.mock.Mock` instances
|
# Propagate any other children as the `child_mock_type` instances
|
||||||
# rather than `self.__class__` instances
|
# rather than `self.__class__` instances
|
||||||
return self.child_mock_type(**kwargs)
|
return self.child_mock_type(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CustomMockMixin(UnpropagatingMockMixin):
|
class CustomMockMixin(UnpropagatingMockMixin[T_Mock], Generic[T_Mock]):
|
||||||
"""Provides common functionality for our custom mock types.
|
"""Provides common functionality for our custom mock types.
|
||||||
|
|
||||||
* Stops propagation of same ``spec_set`` restricted mock in child mocks
|
* Stops propagation of same ``spec_set`` restricted mock in child mocks
|
||||||
|
@ -97,9 +97,16 @@ class CustomMockMixin(UnpropagatingMockMixin):
|
||||||
* Allows using the ``spec_set`` attribute as class attribute
|
* Allows using the ``spec_set`` attribute as class attribute
|
||||||
"""
|
"""
|
||||||
|
|
||||||
spec_set = None
|
spec_set: ClassVar[object] = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs: object):
|
||||||
|
# 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.
|
||||||
if "spec_set" in kwargs:
|
if "spec_set" in kwargs:
|
||||||
self.spec_set = kwargs.pop("spec_set")
|
spec_set = kwargs.pop("spec_set")
|
||||||
super().__init__(spec_set=self.spec_set, **kwargs) # type: ignore # Mixin class, this __init__ is valid
|
else:
|
||||||
|
spec_set = self.spec_set
|
||||||
|
|
||||||
|
super().__init__(spec_set=spec_set, **kwargs) # pyright: ignore[reportCallIssue] # Mixin class, this __init__ is valid
|
||||||
|
|
58
tests/test_helpers.py
Normal file
58
tests/test_helpers.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
from unittest.mock import MagicMock, Mock
|
||||||
|
|
||||||
|
from tests.helpers import CustomMockMixin, UnpropagatingMockMixin, synchronize
|
||||||
|
|
||||||
|
|
||||||
|
def test_synchronize():
|
||||||
|
"""Test the :func:`synchronize` helper function."""
|
||||||
|
|
||||||
|
async def test_func(x: int) -> int:
|
||||||
|
if x == 5:
|
||||||
|
return 10
|
||||||
|
return 0
|
||||||
|
|
||||||
|
assert synchronize(test_func)(5) == 10
|
||||||
|
assert synchronize(test_func)(6) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_unpropagating_mock_mixin():
|
||||||
|
"""Test the :class:`UnpropagatingMockMixin` helper class.
|
||||||
|
|
||||||
|
Mocks that inherit from this mixin should not propagate themselves when new attributes are accessed.
|
||||||
|
Instead, a general mock (of the generic type) should be returned. By default, this is a :class:`MagicMock`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MyUnpropagatingMock(UnpropagatingMockMixin[MagicMock], Mock): ...
|
||||||
|
|
||||||
|
x = MyUnpropagatingMock(spec_set=str)
|
||||||
|
|
||||||
|
# Test that the `spec_set` works as expected
|
||||||
|
assert hasattr(x, "removesuffix")
|
||||||
|
assert not hasattr(x, "notastringmethod")
|
||||||
|
|
||||||
|
unpropagated_mock = x.removesuffix()
|
||||||
|
|
||||||
|
# Test that the resulting mock behaves without spec_set
|
||||||
|
assert hasattr(unpropagated_mock, "removesuffix")
|
||||||
|
assert hasattr(unpropagated_mock, "notastringmethod")
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_mock_mixin():
|
||||||
|
"""Test the :class:`CustomMockMixin` helper class.
|
||||||
|
|
||||||
|
This class is very similar to :class:`UnpropagatingMockMixin`, with the only difference being that it
|
||||||
|
supports setting ``spec_set`` as a class variable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MyCustomMock(CustomMockMixin[MagicMock], Mock): # pyright: ignore[reportUnsafeMultipleInheritance]
|
||||||
|
spec_set = str
|
||||||
|
|
||||||
|
# Test that the mock really has the `spec_set` of `str` by default
|
||||||
|
x = MyCustomMock()
|
||||||
|
assert hasattr(x, "removesuffix")
|
||||||
|
assert not hasattr(x, "notastringmethod")
|
||||||
|
|
||||||
|
# Explicitly setting `spec_set` on __init__ should take precedence
|
||||||
|
y = MyCustomMock(spec_set=int)
|
||||||
|
assert not hasattr(y, "removesuffix")
|
||||||
|
assert hasattr(y, "to_bytes")
|
Loading…
Reference in a new issue