From 354fbd26f7891141c96de57c3e21ce735e4df1a3 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sat, 13 Jul 2024 13:42:52 +0200 Subject: [PATCH 1/3] Fix various issues in tests/helpers.py --- tests/helpers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/helpers.py b/tests/helpers.py index d21d6c6..15cf800 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -70,7 +70,7 @@ class UnpropagatingMockMixin(Generic[T_Mock]): _mock_sealed: bool _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. By default, this method creates a new mock instance of the same original class, and passes @@ -89,7 +89,7 @@ class UnpropagatingMockMixin(Generic[T_Mock]): 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. * Stops propagation of same ``spec_set`` restricted mock in child mocks @@ -99,7 +99,7 @@ class CustomMockMixin(UnpropagatingMockMixin): spec_set = None - def __init__(self, **kwargs): + def __init__(self, **kwargs: object): if "spec_set" in kwargs: self.spec_set = kwargs.pop("spec_set") - super().__init__(spec_set=self.spec_set, **kwargs) # type: ignore # Mixin class, this __init__ is valid + super().__init__(spec_set=self.spec_set, **kwargs) # pyright: ignore[reportCallIssue] # Mixin class, this __init__ is valid From 53a4583f9e3a82245a0c1e962353415ab8cedfc3 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sat, 13 Jul 2024 17:55:58 +0200 Subject: [PATCH 2/3] Update helpers.py --- tests/helpers.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/helpers.py b/tests/helpers.py index 15cf800..161b793 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio 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 @@ -84,7 +84,7 @@ class UnpropagatingMockMixin(Generic[T_Mock]): 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.") - # 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 return self.child_mock_type(**kwargs) @@ -97,9 +97,16 @@ class CustomMockMixin(UnpropagatingMockMixin[T_Mock], Generic[T_Mock]): * Allows using the ``spec_set`` attribute as class attribute """ - spec_set = None + spec_set: ClassVar[object] = None 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: - self.spec_set = kwargs.pop("spec_set") - super().__init__(spec_set=self.spec_set, **kwargs) # pyright: ignore[reportCallIssue] # Mixin class, this __init__ is valid + 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 From 43fe0312ec8e5978f9de16f6b718e28d05cb5b7d Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sat, 13 Jul 2024 17:56:08 +0200 Subject: [PATCH 3/3] Add tests for helpers.py --- tests/test_helpers.py | 58 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test_helpers.py diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..194cf22 --- /dev/null +++ b/tests/test_helpers.py @@ -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")