decoy 2.3.0__py3-none-any.whl → 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ import warnings
2
+ from typing import Any, Callable, Generic, ParamSpec, TypeVar
3
+
4
+ from .errors import createVerifyError
5
+ from .inspect import bind_args
6
+ from .state import DecoyState
7
+ from .values import (
8
+ AttributeEvent,
9
+ CallEvent,
10
+ Event,
11
+ EventMatcher,
12
+ MatchOptions,
13
+ MockInfo,
14
+ )
15
+ from .warnings import createRedundantVerifyWarning
16
+
17
+ SpecT = TypeVar("SpecT")
18
+ ParamsT = ParamSpec("ParamsT")
19
+
20
+
21
+ class Verify(Generic[SpecT]):
22
+ """[Verify](./verify.md) how a mock was called."""
23
+
24
+ def __init__(
25
+ self,
26
+ state: DecoyState,
27
+ mock: MockInfo,
28
+ match_options: MatchOptions,
29
+ ) -> None:
30
+ self._mock = mock
31
+ self._state = state
32
+ self._match_options = match_options
33
+
34
+ def _verify(self, expected: Event) -> None:
35
+ matcher = EventMatcher(expected, self._match_options)
36
+ result = self._state.use_verification(self._mock, matcher)
37
+
38
+ if not result.is_success:
39
+ raise createVerifyError(
40
+ self._mock.name,
41
+ self._match_options,
42
+ expected,
43
+ result.mock_events,
44
+ )
45
+
46
+ if result.is_redundant:
47
+ warnings.warn(
48
+ createRedundantVerifyWarning(self._mock.name, expected),
49
+ stacklevel=3,
50
+ )
51
+
52
+ def called_with(
53
+ self: "Verify[Callable[ParamsT, Any]]",
54
+ *args: ParamsT.args,
55
+ **kwargs: ParamsT.kwargs,
56
+ ) -> None:
57
+ """Verify that a mock was called."""
58
+ bound_args = bind_args(
59
+ signature=self._mock.signature,
60
+ args=args,
61
+ kwargs=kwargs,
62
+ ignore_extra_args=self._match_options.ignore_extra_args,
63
+ )
64
+ expected = CallEvent(args=bound_args.args, kwargs=bound_args.kwargs)
65
+
66
+ self._verify(expected)
67
+
68
+ def set(self, value: SpecT) -> None:
69
+ """Verify that an [attribute was set](./attributes.md#verify-a-setter)."""
70
+ expected = AttributeEvent.set(value)
71
+
72
+ self._verify(expected)
73
+
74
+ def delete(self) -> None:
75
+ """Verify that an [attribute was deleted](./attributes.md#verify-a-deleter)."""
76
+ expected = AttributeEvent.delete()
77
+
78
+ self._verify(expected)
@@ -0,0 +1,35 @@
1
+ from ... import warnings
2
+ from .stringify import count, join_lines, stringify_event, stringify_event_list
3
+ from .values import CallEvent, Event
4
+
5
+
6
+ def createMiscalledStubWarning(
7
+ name: str,
8
+ expected_events: list[Event],
9
+ actual_event: CallEvent,
10
+ ) -> warnings.MiscalledStubWarning:
11
+ """Create a MiscalledStubWarning."""
12
+ message = join_lines(
13
+ "Stub was called but no matching rehearsal found.",
14
+ f"Found {count(len(expected_events), 'rehearsal')}:",
15
+ stringify_event_list(name, expected_events),
16
+ "Found 1 call:",
17
+ f"1.\t{stringify_event(name, actual_event)}",
18
+ )
19
+
20
+ return warnings.MiscalledStubWarning(message)
21
+
22
+
23
+ def createRedundantVerifyWarning(
24
+ name: str,
25
+ event: Event,
26
+ ) -> warnings.RedundantVerifyWarning:
27
+ """Create a RedundantVerifyWarning."""
28
+ message = join_lines(
29
+ "The same rehearsal was used in both a `when` and a `verify`.",
30
+ "This is redundant and probably a misuse of the mock.",
31
+ f"\t{stringify_event(name, event)}",
32
+ "See https://michael.cousins.io/decoy/usage/errors-and-warnings/#redundantverifywarning",
33
+ )
34
+
35
+ return warnings.RedundantVerifyWarning(message)
@@ -0,0 +1,136 @@
1
+ from contextlib import AbstractAsyncContextManager, AbstractContextManager
2
+ from typing import (
3
+ Any,
4
+ Awaitable,
5
+ Callable,
6
+ Generic,
7
+ NoReturn,
8
+ ParamSpec,
9
+ TypeVar,
10
+ overload,
11
+ )
12
+
13
+ from .inspect import bind_args, ensure_callable
14
+ from .state import DecoyState
15
+ from .values import (
16
+ AttributeEvent,
17
+ Behavior,
18
+ CallEvent,
19
+ Event,
20
+ EventMatcher,
21
+ MatchOptions,
22
+ MockInfo,
23
+ )
24
+
25
+ CallableSpecT = TypeVar("CallableSpecT", covariant=True)
26
+ AttributeSpecT = TypeVar("AttributeSpecT")
27
+ ParamsT = ParamSpec("ParamsT")
28
+ ReturnT = TypeVar("ReturnT")
29
+ ContextValueT = TypeVar("ContextValueT")
30
+
31
+
32
+ class Stub(Generic[ParamsT, ReturnT, ContextValueT]):
33
+ """Configure how a mock behaves [when called](./when.md)."""
34
+
35
+ def __init__(
36
+ self,
37
+ state: DecoyState,
38
+ mock: MockInfo,
39
+ matcher: EventMatcher,
40
+ ) -> None:
41
+ self._state = state
42
+ self._mock = mock
43
+ self._matcher = matcher
44
+
45
+ def then_return(self, *values: ReturnT) -> None:
46
+ """Mock a return value."""
47
+ behaviors = [Behavior(return_value=value) for value in values]
48
+ self._push_behaviors(behaviors)
49
+
50
+ def then_enter_with(self, *values: ContextValueT) -> None:
51
+ """Mock a context manager value for a generator context manager."""
52
+ behaviors = [Behavior(context=value) for value in values]
53
+ self._push_behaviors(behaviors)
54
+
55
+ def then_raise(self, *errors: Exception) -> None:
56
+ """Mock a raised exception."""
57
+ behaviors = [Behavior(error=error) for error in errors]
58
+ self._push_behaviors(behaviors)
59
+
60
+ def then_do(self, *actions: Callable[ParamsT, ReturnT]) -> None:
61
+ """Trigger a callback function."""
62
+ behaviors = [
63
+ Behavior(action=ensure_callable(action, is_async=self._mock.is_async))
64
+ for action in actions
65
+ ]
66
+ self._push_behaviors(behaviors)
67
+
68
+ def _push_behaviors(self, behaviors: list[Behavior]) -> None:
69
+ self._state.push_behaviors(self._mock, self._matcher, behaviors)
70
+
71
+
72
+ class When(Generic[CallableSpecT, AttributeSpecT]):
73
+ """Configure [when a mock is triggered](./when.md)."""
74
+
75
+ def __init__(
76
+ self,
77
+ state: DecoyState,
78
+ mock: MockInfo,
79
+ match_options: MatchOptions,
80
+ ) -> None:
81
+ self._state = state
82
+ self._mock = mock
83
+ self._match_options = match_options
84
+
85
+ @overload
86
+ def called_with(
87
+ self: "When[Callable[ParamsT, AbstractAsyncContextManager[ContextValueT] | AbstractContextManager[ContextValueT]], Callable[..., ReturnT]]",
88
+ *args: ParamsT.args,
89
+ **kwargs: ParamsT.kwargs,
90
+ ) -> Stub[ParamsT, ReturnT, ContextValueT]: ...
91
+
92
+ @overload
93
+ def called_with(
94
+ self: "When[Callable[ParamsT, Awaitable[ReturnT]], Any]",
95
+ *args: ParamsT.args,
96
+ **kwargs: ParamsT.kwargs,
97
+ ) -> Stub[ParamsT, ReturnT | Awaitable[ReturnT], NoReturn]: ...
98
+
99
+ @overload
100
+ def called_with(
101
+ self: "When[Callable[ParamsT, ReturnT], Any]",
102
+ *args: ParamsT.args,
103
+ **kwargs: ParamsT.kwargs,
104
+ ) -> Stub[ParamsT, ReturnT, NoReturn]: ...
105
+
106
+ def called_with(
107
+ self,
108
+ *args: Any,
109
+ **kwargs: Any,
110
+ ) -> Stub[Any, Any, Any]:
111
+ """Configure a stub to react to certain passed-in arguments."""
112
+ bound_args = bind_args(
113
+ signature=self._mock.signature,
114
+ args=args,
115
+ kwargs=kwargs,
116
+ ignore_extra_args=self._match_options.ignore_extra_args,
117
+ )
118
+ event = CallEvent(args=bound_args.args, kwargs=bound_args.kwargs)
119
+
120
+ return self._create_stub(event)
121
+
122
+ def get(self) -> Stub[[], AttributeSpecT, NoReturn]:
123
+ """Configure a stub to react to an attribute get."""
124
+ return self._create_stub(AttributeEvent.get())
125
+
126
+ def set(self, value: AttributeSpecT) -> Stub[[AttributeSpecT], None, NoReturn]:
127
+ """Configure a stub to react to an attribute set."""
128
+ return self._create_stub(AttributeEvent.set(value))
129
+
130
+ def delete(self) -> Stub[[], None, NoReturn]:
131
+ """Configure a stub to react to an attribute delete."""
132
+ return self._create_stub(AttributeEvent.delete())
133
+
134
+ def _create_stub(self, event: Event) -> Stub[Any, Any, NoReturn]:
135
+ matcher = EventMatcher(event=event, options=self._match_options)
136
+ return Stub(self._state, self._mock, matcher)
decoy/spy.py CHANGED
@@ -10,7 +10,7 @@ from typing import Any, ContextManager, Dict, Optional, Type, Union, cast, overl
10
10
 
11
11
  from .call_handler import CallHandler
12
12
  from .spy_core import SpyCore
13
- from .spy_events import SpyCall, SpyEvent, SpyPropAccess, PropAccessType
13
+ from .spy_events import PropAccessType, SpyCall, SpyEvent, SpyPropAccess
14
14
 
15
15
 
16
16
  class BaseSpy(ContextManager[Any]):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: decoy
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Opinionated mocking library for Python
5
5
  Author: Michael Cousins
6
6
  Author-email: Michael Cousins <michael@cousins.io>>
@@ -11,12 +11,13 @@ Classifier: Operating System :: OS Independent
11
11
  Classifier: Topic :: Software Development :: Testing
12
12
  Classifier: Topic :: Software Development :: Testing :: Mocking
13
13
  Classifier: Typing :: Typed
14
+ Requires-Dist: typing-extensions>=4.10.0 ; python_full_version >= '3.10' and python_full_version < '3.13'
14
15
  Requires-Python: >=3.7
15
- Project-URL: Changelog, https://github.com/mcous/decoy/releases
16
- Project-URL: Documentation, https://michael.cousins.io/decoy/
17
16
  Project-URL: Homepage, https://michael.cousins.io/decoy/
18
- Project-URL: Issues, https://github.com/mcous/decoy/issues
17
+ Project-URL: Documentation, https://michael.cousins.io/decoy/
19
18
  Project-URL: Repository, https://github.com/mcous/decoy
19
+ Project-URL: Issues, https://github.com/mcous/decoy/issues
20
+ Project-URL: Changelog, https://github.com/mcous/decoy/releases
20
21
  Description-Content-Type: text/markdown
21
22
 
22
23
  <div align="center">
@@ -0,0 +1,38 @@
1
+ decoy/__init__.py,sha256=v3LUyAxuxFrO2M4Iw0VRX0IM6axFMxPmtgIbBemmwj8,11232
2
+ decoy/call_handler.py,sha256=3MixA4pQs4k_2osq1ug19rXXMafzLECGSZLhltSMoo0,1767
3
+ decoy/context_managers.py,sha256=oYsH99ZPmkaCcqjCrOWInueO5ympHm_ICWmbpFoQzqA,1287
4
+ decoy/core.py,sha256=0YsQFnL4Kw_oDX7jRgOVLw9BGsnNdekymQCd1aq-4sQ,5830
5
+ decoy/errors.py,sha256=3vadsAYM-xpoT_lpgiSJbemtjvomWslC9qLfn6FKQpo,2993
6
+ decoy/matchers.py,sha256=Y55oOJww3mhuCsGn9wzUsVMVw0IzxBAMuaSYwtWgOm4,11630
7
+ decoy/mypy/__init__.py,sha256=GdJCf-gXpYB4SBr-Rm8zCLZQxAUuLjPyUieIWgLmdwA,86
8
+ decoy/mypy/plugin.py,sha256=nCfo4XpDxBRDsInDsvCe6h8t7-1OV0ZCJzIJqTsAU0I,1359
9
+ decoy/next/__init__.py,sha256=RqW1Ry5FIw1nptiEqW9W15u1qCySn_ioTRRdu38GFEk,405
10
+ decoy/next/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ decoy/next/_internal/compare.py,sha256=ab9YY3Y4vJhwfw-Kfg-SHGvsaCkZZbmadSI5cJ4j7x8,4173
12
+ decoy/next/_internal/decoy.py,sha256=Mt8cgRVOofwmT0qP4eReUaTvSPFv765MeOCWP9VIR3M,6995
13
+ decoy/next/_internal/errors.py,sha256=E3gUBWY6O-fWt77YJG67bUpOPkV8ktql8VrnZv7UE6Q,3026
14
+ decoy/next/_internal/inspect.py,sha256=LPbm_3ub621KEr6KO1ODhPhbAyxgYnI8qVM2_jmayz8,6698
15
+ decoy/next/_internal/matcher.py,sha256=UDgPgfBfBM7x67AxBFRrm545U634s6YGcc3QsDTCh2M,9141
16
+ decoy/next/_internal/mock.py,sha256=CQrx3nvGFEJSs-r0VNFyHvojtrajFNIT4nQL3_jz9aM,6558
17
+ decoy/next/_internal/state.py,sha256=kFZweF02aG9yGByPa0gtjrheHQRVdFMJwgi19vj5g7o,8247
18
+ decoy/next/_internal/stringify.py,sha256=jQIi9ma7lO2m-Qc-eIth793KnBg7OTRuwr2fg-Q1h-A,2533
19
+ decoy/next/_internal/values.py,sha256=W5yUnDgVNsTfM5qdvc2JZaZKMZ0yC8WES3WIDNZM_WY,1871
20
+ decoy/next/_internal/verify.py,sha256=CEgb7Di-uJskObO2hIycvCgTLixVd4BYCZvHkTvWXRc,2203
21
+ decoy/next/_internal/warnings.py,sha256=59T3hs3x2AcGp6jAUEEbq2pLgFMKO2QatE-1ep0Tlxg,1182
22
+ decoy/next/_internal/when.py,sha256=WtV2U_LIvGbkYgo8CfocUxo3ZDjdlnbV8gtUp6lmBSU,4376
23
+ decoy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ decoy/pytest_plugin.py,sha256=muiWN6zwQPD-AaL4YH6usp-25P4EYjrROTx6wdLE9hM,853
25
+ decoy/spy.py,sha256=G3rEHAx2yWQaucOs8fQoB-Hap8vejPrzYcxBca6XbeQ,7581
26
+ decoy/spy_core.py,sha256=b3PTgCkYnijT08ak_iRUKnA95FnmspSRmT8cs_YtjtM,7965
27
+ decoy/spy_events.py,sha256=PFTfkgN7CVWfbVoSSUMSaCSxfRrUMd7U419iwQ1Xofg,2383
28
+ decoy/spy_log.py,sha256=5JcjcH7zHrn12jp7xyrs_UWY7ATcWJkQ4v9oCR_Cclg,3929
29
+ decoy/stringify.py,sha256=6JA2VA9eK-4AtwSSZPnMYT8ucW7EMpGdbTXokbOpgOg,2240
30
+ decoy/stub_store.py,sha256=tVqKOW65NjotVdDhDDXpLfwagKHypMS4WMob71RYk1c,1744
31
+ decoy/types.py,sha256=LXzky6T0RXvS53NoPpVgCi3KVV6KCyB6EvnBl0NaKug,404
32
+ decoy/verifier.py,sha256=3ZVqaUJPjaNM9Nue3FK6Zzj15qWskX2CEQXoe_yjUEs,1586
33
+ decoy/warning_checker.py,sha256=OOkcrr_I98Pzs3o9UUNgaihJDHwNiu1LlcEK9hi5ZFs,3577
34
+ decoy/warnings.py,sha256=DLdX1ABPk6QvhJWt5s98uR7rCQFQdJJjSPPXRSX0nXA,3203
35
+ decoy-2.4.0.dist-info/WHEEL,sha256=5DEXXimM34_d4Gx1AuF9ysMr1_maoEtGKjaILM3s4w4,80
36
+ decoy-2.4.0.dist-info/entry_points.txt,sha256=En3V0KZt-83nTeWFpVB5KTotgxuUETePl7HesB81W64,40
37
+ decoy-2.4.0.dist-info/METADATA,sha256=LRHd8eo9hYOfpBqBQL8xCcJe4V3p5toYhgyBBtbJBf4,6980
38
+ decoy-2.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.22
2
+ Generator: uv 0.9.29
3
3
  Root-Is-Purelib: true
4
- Tag: py3-none-any
4
+ Tag: py3-none-any
@@ -1,24 +0,0 @@
1
- decoy/__init__.py,sha256=v3LUyAxuxFrO2M4Iw0VRX0IM6axFMxPmtgIbBemmwj8,11232
2
- decoy/call_handler.py,sha256=fuhWr9VAnh0FUvbfWw11fpdQ_GCUE3Hah-EXvdk3gGw,1512
3
- decoy/context_managers.py,sha256=oYsH99ZPmkaCcqjCrOWInueO5ympHm_ICWmbpFoQzqA,1287
4
- decoy/core.py,sha256=0YsQFnL4Kw_oDX7jRgOVLw9BGsnNdekymQCd1aq-4sQ,5830
5
- decoy/errors.py,sha256=mmUAOoQQyXhCLjMcf_3bHMz3PjU7bingIU6sC3hk2B4,3119
6
- decoy/matchers.py,sha256=2zO0F4tbxvLeRlm9BMIAQr_nxX5PBKA4CRJ5Hy5eukU,11724
7
- decoy/mypy/__init__.py,sha256=GdJCf-gXpYB4SBr-Rm8zCLZQxAUuLjPyUieIWgLmdwA,86
8
- decoy/mypy/plugin.py,sha256=nCfo4XpDxBRDsInDsvCe6h8t7-1OV0ZCJzIJqTsAU0I,1359
9
- decoy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- decoy/pytest_plugin.py,sha256=muiWN6zwQPD-AaL4YH6usp-25P4EYjrROTx6wdLE9hM,853
11
- decoy/spy.py,sha256=cxlwDFxd2dcTb6vFJJGc0B3IuFCPJs2cgVMeB0qNXW4,7581
12
- decoy/spy_core.py,sha256=b3PTgCkYnijT08ak_iRUKnA95FnmspSRmT8cs_YtjtM,7965
13
- decoy/spy_events.py,sha256=PFTfkgN7CVWfbVoSSUMSaCSxfRrUMd7U419iwQ1Xofg,2383
14
- decoy/spy_log.py,sha256=5JcjcH7zHrn12jp7xyrs_UWY7ATcWJkQ4v9oCR_Cclg,3929
15
- decoy/stringify.py,sha256=6JA2VA9eK-4AtwSSZPnMYT8ucW7EMpGdbTXokbOpgOg,2240
16
- decoy/stub_store.py,sha256=tVqKOW65NjotVdDhDDXpLfwagKHypMS4WMob71RYk1c,1744
17
- decoy/types.py,sha256=LXzky6T0RXvS53NoPpVgCi3KVV6KCyB6EvnBl0NaKug,404
18
- decoy/verifier.py,sha256=3ZVqaUJPjaNM9Nue3FK6Zzj15qWskX2CEQXoe_yjUEs,1586
19
- decoy/warning_checker.py,sha256=OOkcrr_I98Pzs3o9UUNgaihJDHwNiu1LlcEK9hi5ZFs,3577
20
- decoy/warnings.py,sha256=DLdX1ABPk6QvhJWt5s98uR7rCQFQdJJjSPPXRSX0nXA,3203
21
- decoy-2.3.0.dist-info/WHEEL,sha256=KSLUh82mDPEPk0Bx0ScXlWL64bc8KmzIPNcpQZFV-6E,79
22
- decoy-2.3.0.dist-info/entry_points.txt,sha256=En3V0KZt-83nTeWFpVB5KTotgxuUETePl7HesB81W64,40
23
- decoy-2.3.0.dist-info/METADATA,sha256=3QTINwIfutMyuMNSY0m7TFMHA8Kt5Bl-ijDIqD1I7jA,6874
24
- decoy-2.3.0.dist-info/RECORD,,