decoy 2.2.2__tar.gz → 2.4.0__tar.gz

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.
Files changed (39) hide show
  1. {decoy-2.2.2 → decoy-2.4.0}/PKG-INFO +9 -17
  2. {decoy-2.2.2 → decoy-2.4.0}/README.md +1 -8
  3. {decoy-2.2.2 → decoy-2.4.0}/decoy/call_handler.py +7 -2
  4. {decoy-2.2.2 → decoy-2.4.0}/decoy/errors.py +28 -26
  5. {decoy-2.2.2 → decoy-2.4.0}/decoy/matchers.py +63 -48
  6. decoy-2.4.0/decoy/next/__init__.py +21 -0
  7. decoy-2.4.0/decoy/next/_internal/compare.py +138 -0
  8. decoy-2.4.0/decoy/next/_internal/decoy.py +231 -0
  9. decoy-2.4.0/decoy/next/_internal/errors.py +103 -0
  10. decoy-2.4.0/decoy/next/_internal/inspect.py +229 -0
  11. decoy-2.4.0/decoy/next/_internal/matcher.py +328 -0
  12. decoy-2.4.0/decoy/next/_internal/mock.py +220 -0
  13. decoy-2.4.0/decoy/next/_internal/state.py +265 -0
  14. decoy-2.4.0/decoy/next/_internal/stringify.py +80 -0
  15. decoy-2.4.0/decoy/next/_internal/values.py +96 -0
  16. decoy-2.4.0/decoy/next/_internal/verify.py +78 -0
  17. decoy-2.4.0/decoy/next/_internal/warnings.py +35 -0
  18. decoy-2.4.0/decoy/next/_internal/when.py +136 -0
  19. decoy-2.4.0/decoy/py.typed +0 -0
  20. {decoy-2.2.2 → decoy-2.4.0}/decoy/spy.py +1 -1
  21. decoy-2.4.0/pyproject.toml +120 -0
  22. decoy-2.2.2/LICENSE +0 -21
  23. decoy-2.2.2/pyproject.toml +0 -115
  24. {decoy-2.2.2 → decoy-2.4.0}/decoy/__init__.py +0 -0
  25. {decoy-2.2.2 → decoy-2.4.0}/decoy/context_managers.py +0 -0
  26. {decoy-2.2.2 → decoy-2.4.0}/decoy/core.py +0 -0
  27. {decoy-2.2.2 → decoy-2.4.0}/decoy/mypy/__init__.py +0 -0
  28. {decoy-2.2.2 → decoy-2.4.0}/decoy/mypy/plugin.py +0 -0
  29. /decoy-2.2.2/decoy/py.typed → /decoy-2.4.0/decoy/next/_internal/__init__.py +0 -0
  30. {decoy-2.2.2 → decoy-2.4.0}/decoy/pytest_plugin.py +0 -0
  31. {decoy-2.2.2 → decoy-2.4.0}/decoy/spy_core.py +0 -0
  32. {decoy-2.2.2 → decoy-2.4.0}/decoy/spy_events.py +0 -0
  33. {decoy-2.2.2 → decoy-2.4.0}/decoy/spy_log.py +0 -0
  34. {decoy-2.2.2 → decoy-2.4.0}/decoy/stringify.py +0 -0
  35. {decoy-2.2.2 → decoy-2.4.0}/decoy/stub_store.py +0 -0
  36. {decoy-2.2.2 → decoy-2.4.0}/decoy/types.py +0 -0
  37. {decoy-2.2.2 → decoy-2.4.0}/decoy/verifier.py +0 -0
  38. {decoy-2.2.2 → decoy-2.4.0}/decoy/warning_checker.py +0 -0
  39. {decoy-2.2.2 → decoy-2.4.0}/decoy/warnings.py +0 -0
@@ -1,23 +1,23 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: decoy
3
- Version: 2.2.2
3
+ Version: 2.4.0
4
4
  Summary: Opinionated mocking library for Python
5
- License-Expression: MIT
6
- License-File: LICENSE
7
5
  Author: Michael Cousins
8
- Author-email: michael@cousins.io>
9
- Requires-Python: >=3.7
6
+ Author-email: Michael Cousins <michael@cousins.io>>
7
+ License-Expression: MIT
10
8
  Classifier: Development Status :: 5 - Production/Stable
11
9
  Classifier: Intended Audience :: Developers
12
10
  Classifier: Operating System :: OS Independent
13
11
  Classifier: Topic :: Software Development :: Testing
14
12
  Classifier: Topic :: Software Development :: Testing :: Mocking
15
13
  Classifier: Typing :: Typed
16
- Project-URL: Changelog, https://github.com/mcous/decoy/releases
17
- Project-URL: Documentation, https://michael.cousins.io/decoy/
14
+ Requires-Dist: typing-extensions>=4.10.0 ; python_full_version >= '3.10' and python_full_version < '3.13'
15
+ Requires-Python: >=3.7
18
16
  Project-URL: Homepage, https://michael.cousins.io/decoy/
19
- Project-URL: Issues, https://github.com/mcous/decoy/issues
17
+ Project-URL: Documentation, https://michael.cousins.io/decoy/
20
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
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23
  <div align="center">
@@ -30,7 +30,7 @@ Description-Content-Type: text/markdown
30
30
  <a title="Code Coverage" href="https://app.codecov.io/gh/mcous/decoy/"><img src="https://img.shields.io/codecov/c/github/mcous/decoy?style=flat-square"></a>
31
31
  <a title="License" href="https://github.com/mcous/decoy/blob/main/LICENSE"><img src="https://img.shields.io/github/license/mcous/decoy?style=flat-square"></a>
32
32
  <a title="PyPI Version"href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/pypi/v/decoy?style=flat-square"></a>
33
- <a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/pypi/pyversions/decoy?style=flat-square"></a>
33
+ <a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/python/required-version-toml?style=flat-square&tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fmcous%2Fdecoy%2Fmain%2Fpyproject.toml"></a>
34
34
  </p>
35
35
  <p>
36
36
  <a href="https://michael.cousins.io/decoy/" class="decoy-hidden">Usage guide and documentation</a>
@@ -44,14 +44,7 @@ Decoy mocks are **async/await** and **type-checking** friendly. Decoy is heavily
44
44
  ## Install
45
45
 
46
46
  ```bash
47
- # pip
48
47
  pip install decoy
49
-
50
- # poetry
51
- poetry add --dev decoy
52
-
53
- # pipenv
54
- pipenv install --dev decoy
55
48
  ```
56
49
 
57
50
  ## Setup
@@ -171,4 +164,3 @@ See [spying with verify][] for more details.
171
164
  [creating mocks]: https://michael.cousins.io/decoy/usage/create/
172
165
  [stubbing with when]: https://michael.cousins.io/decoy/usage/when/
173
166
  [spying with verify]: https://michael.cousins.io/decoy/usage/verify/
174
-
@@ -8,7 +8,7 @@
8
8
  <a title="Code Coverage" href="https://app.codecov.io/gh/mcous/decoy/"><img src="https://img.shields.io/codecov/c/github/mcous/decoy?style=flat-square"></a>
9
9
  <a title="License" href="https://github.com/mcous/decoy/blob/main/LICENSE"><img src="https://img.shields.io/github/license/mcous/decoy?style=flat-square"></a>
10
10
  <a title="PyPI Version"href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/pypi/v/decoy?style=flat-square"></a>
11
- <a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/pypi/pyversions/decoy?style=flat-square"></a>
11
+ <a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/python/required-version-toml?style=flat-square&tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fmcous%2Fdecoy%2Fmain%2Fpyproject.toml"></a>
12
12
  </p>
13
13
  <p>
14
14
  <a href="https://michael.cousins.io/decoy/" class="decoy-hidden">Usage guide and documentation</a>
@@ -22,14 +22,7 @@ Decoy mocks are **async/await** and **type-checking** friendly. Decoy is heavily
22
22
  ## Install
23
23
 
24
24
  ```bash
25
- # pip
26
25
  pip install decoy
27
-
28
- # poetry
29
- poetry add --dev decoy
30
-
31
- # pipenv
32
- pipenv install --dev decoy
33
26
  ```
34
27
 
35
28
  ## Setup
@@ -2,9 +2,9 @@
2
2
 
3
3
  from typing import Any, NamedTuple, Optional
4
4
 
5
- from .spy_log import SpyLog
6
5
  from .context_managers import ContextWrapper
7
- from .spy_events import SpyCall, SpyEvent
6
+ from .spy_events import PropAccessType, SpyCall, SpyEvent, SpyPropAccess
7
+ from .spy_log import SpyLog
8
8
  from .stub_store import MISSING, StubStore
9
9
 
10
10
 
@@ -41,6 +41,11 @@ class CallHandler:
41
41
  *call.payload.args,
42
42
  **call.payload.kwargs,
43
43
  )
44
+ elif (
45
+ isinstance(call.payload, SpyPropAccess)
46
+ and call.payload.access_type == PropAccessType.SET
47
+ ):
48
+ return_value = behavior.action(call.payload.value)
44
49
  else:
45
50
  return_value = behavior.action()
46
51
 
@@ -12,12 +12,7 @@ from .stringify import count, stringify_error_message
12
12
 
13
13
 
14
14
  class MockNameRequiredError(ValueError):
15
- """An error raised if a name is not provided for a mock.
16
-
17
- See the [MockNameRequiredError guide][] for more details.
18
-
19
- [MockNameRequiredError guide]: usage/errors-and-warnings.md#mocknamerequirederror
20
- """
15
+ """A name was not provided for a mock."""
21
16
 
22
17
  @classmethod
23
18
  def create(cls) -> "MockNameRequiredError":
@@ -25,17 +20,17 @@ class MockNameRequiredError(ValueError):
25
20
  return cls("Mocks without `cls` or `func` require a `name`.")
26
21
 
27
22
 
23
+ class MockSpecInvalidError(TypeError):
24
+ """A value passed as a mock spec is not valid."""
25
+
26
+
28
27
  class MissingRehearsalError(ValueError):
29
- """An error raised when a Decoy method is called without rehearsal(s).
28
+ """A Decoy method was called without rehearsal(s).
30
29
 
31
30
  This error is raised if you use [`when`][decoy.Decoy.when],
32
31
  [`verify`][decoy.Decoy.verify], or [`prop`][decoy.Decoy.prop] incorrectly
33
32
  in your tests. When using async/await, this error can be triggered if you
34
33
  forget to include `await` with your rehearsal.
35
-
36
- See the [MissingRehearsalError guide][] for more details.
37
-
38
- [MissingRehearsalError guide]: usage/errors-and-warnings.md#missingrehearsalerror
39
34
  """
40
35
 
41
36
  @classmethod
@@ -44,29 +39,28 @@ class MissingRehearsalError(ValueError):
44
39
  return cls("Rehearsal not found.")
45
40
 
46
41
 
42
+ class NotAMockError(TypeError):
43
+ """A Decoy method was called without a mock."""
44
+
45
+
46
+ class ThenDoActionNotCallableError(TypeError):
47
+ """A value passed to `then_do` is not callable."""
48
+
49
+
47
50
  class MockNotAsyncError(TypeError):
48
- """An error raised when an asynchronous function is used with a synchronous mock.
51
+ """An asynchronous function was passed to a synchronous mock.
49
52
 
50
53
  This error is raised if you pass an `async def` function
51
- to a synchronous stub's `then_do` method.
52
- See the [MockNotAsyncError guide][] for more details.
53
-
54
- [MockNotAsyncError guide]: usage/errors-and-warnings.md#mocknotasyncerror
54
+ to a synchronous stub's [`then_do`][decoy.Stub.then_do] method.
55
55
  """
56
56
 
57
57
 
58
- class VerifyError(AssertionError):
59
- """An error raised when actual calls do not match rehearsals given to `verify`.
58
+ class SignatureMismatchError(TypeError):
59
+ """Arguments did not match the signature of the mock."""
60
60
 
61
- See [spying with verify][] for more details.
62
61
 
63
- [spying with verify]: usage/verify.md
64
-
65
- Attributes:
66
- rehearsals: Rehearsals that were being verified.
67
- calls: Actual calls to the mock(s).
68
- times: The expected number of calls to the mock, if any.
69
- """
62
+ class VerifyError(AssertionError):
63
+ """A [`Decoy.verify`][decoy.Decoy.verify] assertion failed."""
70
64
 
71
65
  rehearsals: Sequence[VerifyRehearsal]
72
66
  calls: Sequence[SpyEvent]
@@ -100,3 +94,11 @@ class VerifyError(AssertionError):
100
94
  result.times = times
101
95
 
102
96
  return result
97
+
98
+
99
+ class VerifyOrderError(VerifyError):
100
+ """A [`Decoy.verify_order`][decoy.next.Decoy.verify_order] assertion failed."""
101
+
102
+
103
+ class NoMatcherValueCapturedError(ValueError):
104
+ """An error raised if a [decoy.next.Matcher][] has not captured any matching values."""
@@ -28,16 +28,17 @@ See the [matchers guide][] for more details.
28
28
  """
29
29
 
30
30
  from re import compile as compile_re
31
- from typing import Any, List, Mapping, Optional, Pattern, Type, TypeVar, cast
32
-
33
- __all__ = [
34
- "Anything",
35
- "Captor",
36
- "ErrorMatching",
37
- "IsA",
38
- "IsNot",
39
- "StringMatching",
40
- ]
31
+ from typing import (
32
+ Any,
33
+ Generic,
34
+ List,
35
+ Mapping,
36
+ Optional,
37
+ Pattern,
38
+ Type,
39
+ TypeVar,
40
+ cast,
41
+ )
41
42
 
42
43
 
43
44
  class _AnythingOrNone:
@@ -177,14 +178,11 @@ class _HasAttributes:
177
178
 
178
179
  def __eq__(self, target: object) -> bool:
179
180
  """Return true if target matches all given attributes."""
180
- is_match = True
181
181
  for attr_name, value in self._attributes.items():
182
- if is_match:
183
- is_match = (
184
- hasattr(target, attr_name) and getattr(target, attr_name) == value
185
- )
182
+ if not hasattr(target, attr_name) or getattr(target, attr_name) != value:
183
+ return False
186
184
 
187
- return is_match
185
+ return True
188
186
 
189
187
  def __repr__(self) -> str:
190
188
  """Return a string representation of the matcher."""
@@ -218,16 +216,14 @@ class _DictMatching:
218
216
 
219
217
  def __eq__(self, target: object) -> bool:
220
218
  """Return true if target matches all given keys/values."""
221
- is_match = True
222
-
223
219
  for key, value in self._values.items():
224
- if is_match:
225
- try:
226
- is_match = key in target and target[key] == value # type: ignore[index,operator]
227
- except TypeError:
228
- is_match = False
220
+ try:
221
+ if key not in target or target[key] != value: # type: ignore[index,operator]
222
+ return False
223
+ except TypeError:
224
+ return False
229
225
 
230
- return is_match
226
+ return True
231
227
 
232
228
  def __repr__(self) -> str:
233
229
  """Return a string representation of the matcher."""
@@ -318,10 +314,12 @@ def StringMatching(match: str) -> str:
318
314
  class _ErrorMatching:
319
315
  _error_type: Type[BaseException]
320
316
  _string_matcher: Optional[_StringMatching]
317
+ _match: Optional[str]
321
318
 
322
319
  def __init__(self, error: Type[BaseException], match: Optional[str] = None) -> None:
323
320
  """Initialize with the Exception type and optional message matcher."""
324
321
  self._error_type = error
322
+ self._match = match
325
323
  self._string_matcher = _StringMatching(match) if match is not None else None
326
324
 
327
325
  def __eq__(self, target: object) -> bool:
@@ -337,9 +335,7 @@ class _ErrorMatching:
337
335
 
338
336
  def __repr__(self) -> str:
339
337
  """Return a string representation of the matcher."""
340
- return (
341
- f"<ErrorMatching {self._error_type.__name__} match={self._string_matcher}>"
342
- )
338
+ return f"<ErrorMatching {self._error_type.__name__} match={self._match!r}>"
343
339
 
344
340
 
345
341
  ErrorT = TypeVar("ErrorT", bound=BaseException)
@@ -361,12 +357,32 @@ def ErrorMatching(error: Type[ErrorT], match: Optional[str] = None) -> ErrorT:
361
357
  return cast(ErrorT, _ErrorMatching(error, match))
362
358
 
363
359
 
364
- class _Captor:
360
+ CapturedT = TypeVar("CapturedT")
361
+
362
+
363
+ class ValueCaptor(Generic[CapturedT]):
364
+ """Match anything, capturing its value for further assertions.
365
+
366
+ Compare against the `matcher` property to capture a value.
367
+ The last captured value is available via `captor.value`,
368
+ while all captured values are stored in `captor.values`.
369
+
370
+ !!! example
371
+ ```python
372
+ captor = ValueCaptor[str]()
373
+ assert "foobar" == captor.matcher
374
+ print(captor.value) # "foobar"
375
+ print(captor.values) # ["foobar"]
376
+ ```
377
+ """
378
+
379
+ _values: List[object]
380
+
365
381
  def __init__(self) -> None:
366
- self._values: List[Any] = []
382
+ self._values = []
367
383
 
368
384
  def __eq__(self, target: object) -> bool:
369
- """Capture compared value, always returning True."""
385
+ """Captors are always "equal" to a given target."""
370
386
  self._values.append(target)
371
387
  return True
372
388
 
@@ -375,11 +391,19 @@ class _Captor:
375
391
  return "<Captor>"
376
392
 
377
393
  @property
378
- def value(self) -> Any:
379
- """Get the captured value.
394
+ def matcher(self) -> CapturedT:
395
+ """Match anything, capturing its value.
396
+
397
+ This method exists as a type-checking convenience.
398
+ """
399
+ return cast(CapturedT, self)
400
+
401
+ @property
402
+ def value(self) -> object:
403
+ """The latest captured value.
380
404
 
381
405
  Raises:
382
- AssertionError: if no value was captured.
406
+ AssertionError: no value has been captured.
383
407
  """
384
408
  if len(self._values) == 0:
385
409
  raise AssertionError("No value captured by captor.")
@@ -387,24 +411,15 @@ class _Captor:
387
411
  return self._values[-1]
388
412
 
389
413
  @property
390
- def values(self) -> List[Any]:
391
- """Get all captured values."""
414
+ def values(self) -> List[object]:
415
+ """All captured values."""
392
416
  return self._values
393
417
 
394
418
 
395
419
  def Captor() -> Any:
396
- """Match anything, capturing its value.
420
+ """Match anything, capturing its value for further assertions.
397
421
 
398
- The last captured value will be set to `captor.value`. All captured
399
- values will be placed in the `captor.values` list, which can be
400
- helpful if a captor needs to be triggered multiple times.
401
-
402
- !!! example
403
- ```python
404
- captor = Captor()
405
- assert "foobar" == captor
406
- print(captor.value) # "foobar"
407
- print(captor.values) # ["foobar"]
408
- ```
422
+ !!! tip
423
+ Prefer [decoy.matchers.ValueCaptor][], which has better type annotations.
409
424
  """
410
- return _Captor()
425
+ return ValueCaptor()
@@ -0,0 +1,21 @@
1
+ """Decoy mocking library.
2
+
3
+ Use Decoy to create stubs and spies
4
+ to isolate your code under test.
5
+ """
6
+
7
+ from ._internal.decoy import Decoy
8
+ from ._internal.matcher import Matcher
9
+ from ._internal.mock import AsyncMock, Mock
10
+ from ._internal.verify import Verify
11
+ from ._internal.when import Stub, When
12
+
13
+ __all__ = [
14
+ "AsyncMock",
15
+ "Decoy",
16
+ "Matcher",
17
+ "Mock",
18
+ "Stub",
19
+ "Verify",
20
+ "When",
21
+ ]
@@ -0,0 +1,138 @@
1
+ from .values import (
2
+ AttributeEvent,
3
+ AttributeEventType,
4
+ BehaviorEntry,
5
+ CallEvent,
6
+ Event,
7
+ EventEntry,
8
+ EventMatcher,
9
+ EventState,
10
+ MockInfo,
11
+ VerificationEntry,
12
+ )
13
+
14
+
15
+ def is_event_from_mock(event_entry: EventEntry, mock: MockInfo) -> bool:
16
+ return mock.id == event_entry.mock.id
17
+
18
+
19
+ def is_verifiable_mock_event(event_entry: EventEntry, mock: MockInfo) -> bool:
20
+ return is_event_from_mock(event_entry, mock) and (
21
+ isinstance(event_entry.event, CallEvent)
22
+ or event_entry.event.type != AttributeEventType.GET
23
+ )
24
+
25
+
26
+ def is_matching_behavior(
27
+ event_entry: EventEntry,
28
+ behavior_entry: BehaviorEntry,
29
+ ) -> bool:
30
+ return is_event_from_mock(
31
+ event_entry,
32
+ behavior_entry.mock,
33
+ ) and is_matching_event(
34
+ event_entry,
35
+ behavior_entry.matcher,
36
+ )
37
+
38
+
39
+ def is_matching_event(event_entry: EventEntry, matcher: EventMatcher) -> bool:
40
+ event_matches = _match_event(event_entry.event, matcher)
41
+ state_matches = _match_state(event_entry.state, matcher)
42
+
43
+ return event_matches and state_matches
44
+
45
+
46
+ def is_matching_count(usage_count: int, matcher: EventMatcher) -> bool:
47
+ return matcher.options.times is None or usage_count < matcher.options.times
48
+
49
+
50
+ def is_successful_verify(verification: VerificationEntry) -> bool:
51
+ if verification.matcher.options.times is not None:
52
+ return len(verification.matching_events) == verification.matcher.options.times
53
+
54
+ return len(verification.matching_events) > 0
55
+
56
+
57
+ def is_successful_verify_order(verifications: list[VerificationEntry]) -> bool:
58
+ matching_events: list[tuple[EventEntry, VerificationEntry]] = []
59
+ verification_index = 0
60
+ event_index = 0
61
+
62
+ for verification in verifications:
63
+ for matching_event in verification.matching_events:
64
+ matching_events.append((matching_event, verification))
65
+
66
+ matching_events.sort(key=lambda e: e[0].order)
67
+
68
+ while event_index < len(matching_events) and verification_index < len(
69
+ verifications
70
+ ):
71
+ _, event_verification = matching_events[event_index]
72
+ expected_verification = verifications[verification_index]
73
+ expected_times = expected_verification.matcher.options.times
74
+ remaining_events = len(matching_events) - event_index
75
+
76
+ if event_verification is expected_verification:
77
+ verification_index += 1
78
+
79
+ if expected_times is None or expected_times == 1:
80
+ event_index += 1
81
+ else:
82
+ for times_index in range(1, expected_times):
83
+ _, later_verification = matching_events[event_index + times_index]
84
+ if later_verification is not expected_verification:
85
+ return False
86
+
87
+ event_index += expected_times
88
+
89
+ elif remaining_events >= len(verifications) and verification_index > 0:
90
+ verification_index = 0
91
+ else:
92
+ return False
93
+
94
+ return True
95
+
96
+
97
+ def is_redundant_verify(
98
+ verification: VerificationEntry,
99
+ behaviors: list[BehaviorEntry],
100
+ ) -> bool:
101
+ return any(
102
+ behavior
103
+ for behavior in behaviors
104
+ if verification.mock.id == behavior.mock.id
105
+ and verification.matcher.options == behavior.matcher.options
106
+ and _match_event(verification.matcher.event, behavior.matcher)
107
+ )
108
+
109
+
110
+ def _match_event(event: Event, matcher: EventMatcher) -> bool:
111
+ if (
112
+ matcher.options.ignore_extra_args is False
113
+ or isinstance(event, AttributeEvent)
114
+ or isinstance(matcher.event, AttributeEvent)
115
+ ):
116
+ return event == matcher.event
117
+
118
+ try:
119
+ args_match = all(
120
+ value == event.args[i] for i, value in enumerate(matcher.event.args)
121
+ )
122
+ kwargs_match = all(
123
+ value == event.kwargs[key] for key, value in matcher.event.kwargs.items()
124
+ )
125
+
126
+ return args_match and kwargs_match
127
+
128
+ except (IndexError, KeyError):
129
+ pass
130
+
131
+ return False
132
+
133
+
134
+ def _match_state(event_state: EventState, matcher: EventMatcher) -> bool:
135
+ return (
136
+ matcher.options.is_entered is None
137
+ or event_state.is_entered == matcher.options.is_entered
138
+ )