Skip to content

Commit fd8a897

Browse files
zlalvanimcous
andauthored
fix: accept message string in error and warning constructors (#272)
Fixes #271 Co-authored-by: Michael Cousins <michael@cousins.io>
1 parent 2915db6 commit fd8a897

9 files changed

Lines changed: 86 additions & 64 deletions

File tree

decoy/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
from typing import Any, Callable, Coroutine, Generic, Optional, Union, overload
44

55
from . import errors, matchers, warnings
6-
from .core import DecoyCore, StubCore, PropCore
7-
from .types import ClassT, ContextValueT, FuncT, ReturnT
86
from .context_managers import (
9-
ContextManager,
107
AsyncContextManager,
11-
GeneratorContextManager,
128
AsyncGeneratorContextManager,
9+
ContextManager,
10+
GeneratorContextManager,
1311
)
12+
from .core import DecoyCore, PropCore, StubCore
13+
from .types import ClassT, ContextValueT, FuncT, ReturnT
1414

1515
# ensure decoy does not pollute pytest tracebacks
1616
__tracebackhide__ = True
@@ -82,7 +82,7 @@ def test_get_something(decoy: Decoy):
8282
spec = cls or func
8383

8484
if spec is None and name is None:
85-
raise errors.MockNameRequiredError()
85+
raise errors.MockNameRequiredError.create()
8686

8787
return self._core.mock(spec=spec, name=name, is_async=is_async)
8888

decoy/errors.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ class MockNameRequiredError(ValueError):
1919
[MockNameRequiredError guide]: usage/errors-and-warnings.md#mocknamerequirederror
2020
"""
2121

22-
def __init__(self) -> None:
23-
super().__init__("Mocks without `cls` or `func` require a `name`.")
22+
@classmethod
23+
def create(cls) -> "MockNameRequiredError":
24+
"""Create a MockNameRequiredError."""
25+
return cls("Mocks without `cls` or `func` require a `name`.")
2426

2527

2628
class MissingRehearsalError(ValueError):
@@ -36,8 +38,10 @@ class MissingRehearsalError(ValueError):
3638
[MissingRehearsalError guide]: usage/errors-and-warnings.md#missingrehearsalerror
3739
"""
3840

39-
def __init__(self) -> None:
40-
super().__init__("Rehearsal not found.")
41+
@classmethod
42+
def create(cls) -> "MissingRehearsalError":
43+
"""Create a MissingRehearsalError."""
44+
return cls("Rehearsal not found.")
4145

4246

4347
class MockNotAsyncError(TypeError):
@@ -68,12 +72,14 @@ class VerifyError(AssertionError):
6872
calls: Sequence[SpyEvent]
6973
times: Optional[int]
7074

71-
def __init__(
72-
self,
75+
@classmethod
76+
def create(
77+
cls,
7378
rehearsals: Sequence[VerifyRehearsal],
7479
calls: Sequence[SpyEvent],
7580
times: Optional[int],
76-
) -> None:
81+
) -> "VerifyError":
82+
"""Create a VerifyError."""
7783
if times is not None:
7884
heading = f"Expected exactly {count(times, 'call')}:"
7985
elif len(rehearsals) == 1:
@@ -88,7 +94,9 @@ def __init__(
8894
include_calls=times is None or times == len(calls),
8995
)
9096

91-
super().__init__(message)
92-
self.rehearsals = rehearsals
93-
self.calls = calls
94-
self.times = times
97+
result = cls(message)
98+
result.rehearsals = rehearsals
99+
result.calls = calls
100+
result.times = times
101+
102+
return result

decoy/spy_log.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
from .errors import MissingRehearsalError
66
from .spy_events import (
77
AnySpyEvent,
8+
PropAccessType,
9+
PropRehearsal,
810
SpyCall,
911
SpyEvent,
1012
SpyPropAccess,
11-
WhenRehearsal,
1213
VerifyRehearsal,
13-
PropAccessType,
14-
PropRehearsal,
14+
WhenRehearsal,
1515
)
1616

1717

@@ -33,10 +33,10 @@ def consume_when_rehearsal(self, ignore_extra_args: bool) -> WhenRehearsal:
3333
try:
3434
event = self._log[-1]
3535
except IndexError as e:
36-
raise MissingRehearsalError() from e
36+
raise MissingRehearsalError.create() from e
3737

3838
if not isinstance(event, SpyEvent):
39-
raise MissingRehearsalError()
39+
raise MissingRehearsalError.create()
4040

4141
spy, payload = _apply_ignore_extra_args(event, ignore_extra_args)
4242

@@ -59,12 +59,12 @@ def consume_verify_rehearsals(
5959

6060
while len(rehearsals) < count:
6161
if index < 0:
62-
raise MissingRehearsalError()
62+
raise MissingRehearsalError.create()
6363

6464
event = self._log[index]
6565

6666
if not isinstance(event, (SpyEvent, PropRehearsal)):
67-
raise MissingRehearsalError()
67+
raise MissingRehearsalError.create()
6868

6969
if _is_verifiable(event):
7070
rehearsal = VerifyRehearsal(
@@ -82,7 +82,7 @@ def consume_prop_rehearsal(self) -> PropRehearsal:
8282
try:
8383
event = self._log[-1]
8484
except IndexError as e:
85-
raise MissingRehearsalError() from e
85+
raise MissingRehearsalError.create() from e
8686

8787
spy, payload = event
8888

@@ -91,7 +91,7 @@ def consume_prop_rehearsal(self) -> PropRehearsal:
9191
or not isinstance(payload, SpyPropAccess)
9292
or payload.access_type != PropAccessType.GET
9393
):
94-
raise MissingRehearsalError()
94+
raise MissingRehearsalError.create()
9595

9696
rehearsal = PropRehearsal(spy, payload)
9797
self._log[-1] = rehearsal

decoy/verifier.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
from typing import Optional, Sequence
44

5-
from .spy_events import SpyEvent, VerifyRehearsal, match_event
65
from .errors import VerifyError
6+
from .spy_events import SpyEvent, VerifyRehearsal, match_event
77

88
# ensure decoy.verifier does not pollute Pytest tracebacks
99
__tracebackhide__ = True
@@ -42,7 +42,7 @@ def verify(
4242
calls_verified = match_count != 0 if times is None else match_count == times
4343

4444
if not calls_verified:
45-
raise VerifyError(
45+
raise VerifyError.create(
4646
rehearsals=rehearsals,
4747
calls=calls,
4848
times=times,

decoy/warning_checker.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
AnySpyEvent,
1010
SpyCall,
1111
SpyEvent,
12+
SpyRehearsal,
1213
VerifyRehearsal,
1314
WhenRehearsal,
14-
SpyRehearsal,
1515
match_event,
1616
)
1717
from .warnings import DecoyWarning, MiscalledStubWarning, RedundantVerifyWarning
@@ -78,7 +78,7 @@ def _check_no_miscalled_stubs(all_events: Sequence[AnySpyEvent]) -> None:
7878

7979
if is_stubbed and all(len(c.matching_rehearsals) == 0 for c in calls):
8080
_warn(
81-
MiscalledStubWarning(
81+
MiscalledStubWarning.create(
8282
calls=[c.event for c in calls],
8383
rehearsals=rehearsals,
8484
)
@@ -91,7 +91,7 @@ def _check_no_redundant_verify(all_calls: Sequence[AnySpyEvent]) -> None:
9191

9292
for vr in verify_rehearsals:
9393
if any(wr for wr in when_rehearsals if wr == vr): # type: ignore[comparison-overlap]
94-
_warn(RedundantVerifyWarning(rehearsal=vr))
94+
_warn(RedundantVerifyWarning.create(rehearsal=vr))
9595

9696

9797
def _warn(warning: DecoyWarning) -> None:

decoy/warnings.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typing import Sequence
1010

1111
from .spy_events import SpyEvent, SpyRehearsal, VerifyRehearsal
12-
from .stringify import stringify_call, stringify_error_message, count
12+
from .stringify import count, stringify_call, stringify_error_message
1313

1414

1515
class DecoyWarning(UserWarning):
@@ -31,18 +31,20 @@ class MiscalledStubWarning(DecoyWarning):
3131
[MiscalledStubWarning guide]: usage/errors-and-warnings.md#miscalledstubwarning
3232
3333
Attributes:
34-
rehearsals: The mocks's configured rehearsals.
34+
rehearsals: The mock's configured rehearsals.
3535
calls: Actual calls to the mock.
3636
"""
3737

3838
rehearsals: Sequence[SpyRehearsal]
3939
calls: Sequence[SpyEvent]
4040

41-
def __init__(
42-
self,
41+
@classmethod
42+
def create(
43+
cls,
4344
rehearsals: Sequence[SpyRehearsal],
4445
calls: Sequence[SpyEvent],
45-
) -> None:
46+
) -> "MiscalledStubWarning":
47+
"""Create a MiscalledStubWarning."""
4648
heading = os.linesep.join(
4749
[
4850
"Stub was called but no matching rehearsal found.",
@@ -56,9 +58,11 @@ def __init__(
5658
calls=calls,
5759
)
5860

59-
super().__init__(message)
60-
self.rehearsals = rehearsals
61-
self.calls = calls
61+
result = cls(message)
62+
result.rehearsals = rehearsals
63+
result.calls = calls
64+
65+
return result
6266

6367

6468
class RedundantVerifyWarning(DecoyWarning):
@@ -74,7 +78,11 @@ class RedundantVerifyWarning(DecoyWarning):
7478
[RedundantVerifyWarning guide]: usage/errors-and-warnings.md#redundantverifywarning
7579
"""
7680

77-
def __init__(self, rehearsal: VerifyRehearsal) -> None:
81+
rehearsal: VerifyRehearsal
82+
83+
@classmethod
84+
def create(cls, rehearsal: VerifyRehearsal) -> "RedundantVerifyWarning":
85+
"""Create a RedundantVerifyWarning."""
7886
message = os.linesep.join(
7987
[
8088
"The same rehearsal was used in both a `when` and a `verify`.",
@@ -83,8 +91,11 @@ def __init__(self, rehearsal: VerifyRehearsal) -> None:
8391
"See https://michael.cousins.io/decoy/usage/errors-and-warnings/#redundantverifywarning",
8492
]
8593
)
86-
super().__init__(message)
87-
self.rehearsal = rehearsal
94+
95+
result = cls(message)
96+
result.rehearsal = rehearsal
97+
98+
return result
8899

89100

90101
class IncorrectCallWarning(DecoyWarning):

tests/test_errors.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Tests for error and warning message generation."""
22

3-
import pytest
43
import os
54
from typing import List, NamedTuple, Optional
65

7-
from decoy.spy_events import SpyCall, SpyEvent, SpyInfo, VerifyRehearsal
6+
import pytest
7+
88
from decoy.errors import VerifyError
9+
from decoy.spy_events import SpyCall, SpyEvent, SpyInfo, VerifyRehearsal
910

1011

1112
class VerifyErrorSpec(NamedTuple):
@@ -166,5 +167,5 @@ def test_verify_error(
166167
expected_message: str,
167168
) -> None:
168169
"""It should stringify VerifyError properly."""
169-
error = VerifyError(rehearsals=rehearsals, calls=calls, times=times)
170+
error = VerifyError.create(rehearsals=rehearsals, calls=calls, times=times)
170171
assert str(error) == expected_message

0 commit comments

Comments
 (0)