decoy 2.2.0__tar.gz → 2.2.2__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.
@@ -1,29 +1,22 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: decoy
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Opinionated mocking library for Python
5
- License: MIT
5
+ License-Expression: MIT
6
+ License-File: LICENSE
6
7
  Author: Michael Cousins
7
- Author-email: michael@cousins.io
8
- Requires-Python: >=3.7,<4.0
8
+ Author-email: michael@cousins.io>
9
+ Requires-Python: >=3.7
9
10
  Classifier: Development Status :: 5 - Production/Stable
10
11
  Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: OS Independent
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.7
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
13
  Classifier: Topic :: Software Development :: Testing
22
14
  Classifier: Topic :: Software Development :: Testing :: Mocking
23
15
  Classifier: Typing :: Typed
24
16
  Project-URL: Changelog, https://github.com/mcous/decoy/releases
25
17
  Project-URL: Documentation, https://michael.cousins.io/decoy/
26
18
  Project-URL: Homepage, https://michael.cousins.io/decoy/
19
+ Project-URL: Issues, https://github.com/mcous/decoy/issues
27
20
  Project-URL: Repository, https://github.com/mcous/decoy
28
21
  Description-Content-Type: text/markdown
29
22
 
@@ -3,14 +3,14 @@
3
3
  from typing import Any, Callable, Coroutine, Generic, Optional, Union, overload
4
4
 
5
5
  from . import errors, matchers, warnings
6
- from .core import DecoyCore, StubCore, PropCore
7
- from .types import ClassT, ContextValueT, FuncT, ReturnT
8
6
  from .context_managers import (
9
- ContextManager,
10
7
  AsyncContextManager,
11
- GeneratorContextManager,
12
8
  AsyncGeneratorContextManager,
9
+ ContextManager,
10
+ GeneratorContextManager,
13
11
  )
12
+ from .core import DecoyCore, PropCore, StubCore
13
+ from .types import ClassT, ContextValueT, FuncT, ReturnT
14
14
 
15
15
  # ensure decoy does not pollute pytest tracebacks
16
16
  __tracebackhide__ = True
@@ -82,7 +82,7 @@ class Decoy:
82
82
  spec = cls or func
83
83
 
84
84
  if spec is None and name is None:
85
- raise errors.MockNameRequiredError()
85
+ raise errors.MockNameRequiredError.create()
86
86
 
87
87
  return self._core.mock(spec=spec, name=name, is_async=is_async)
88
88
 
@@ -19,8 +19,10 @@ class MockNameRequiredError(ValueError):
19
19
  [MockNameRequiredError guide]: usage/errors-and-warnings.md#mocknamerequirederror
20
20
  """
21
21
 
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`.")
24
26
 
25
27
 
26
28
  class MissingRehearsalError(ValueError):
@@ -36,8 +38,10 @@ class MissingRehearsalError(ValueError):
36
38
  [MissingRehearsalError guide]: usage/errors-and-warnings.md#missingrehearsalerror
37
39
  """
38
40
 
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.")
41
45
 
42
46
 
43
47
  class MockNotAsyncError(TypeError):
@@ -68,12 +72,14 @@ class VerifyError(AssertionError):
68
72
  calls: Sequence[SpyEvent]
69
73
  times: Optional[int]
70
74
 
71
- def __init__(
72
- self,
75
+ @classmethod
76
+ def create(
77
+ cls,
73
78
  rehearsals: Sequence[VerifyRehearsal],
74
79
  calls: Sequence[SpyEvent],
75
80
  times: Optional[int],
76
- ) -> None:
81
+ ) -> "VerifyError":
82
+ """Create a VerifyError."""
77
83
  if times is not None:
78
84
  heading = f"Expected exactly {count(times, 'call')}:"
79
85
  elif len(rehearsals) == 1:
@@ -88,7 +94,9 @@ class VerifyError(AssertionError):
88
94
  include_calls=times is None or times == len(calls),
89
95
  )
90
96
 
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
@@ -28,8 +28,7 @@ See the [matchers guide][] for more details.
28
28
  """
29
29
 
30
30
  from re import compile as compile_re
31
- from typing import cast, Any, List, Mapping, Optional, Pattern, Type, TypeVar
32
-
31
+ from typing import Any, List, Mapping, Optional, Pattern, Type, TypeVar, cast
33
32
 
34
33
  __all__ = [
35
34
  "Anything",
@@ -212,9 +211,9 @@ def HasAttributes(attributes: Mapping[str, Any]) -> Any:
212
211
 
213
212
 
214
213
  class _DictMatching:
215
- _values: Mapping[str, Any]
214
+ _values: Mapping[Any, Any]
216
215
 
217
- def __init__(self, values: Mapping[str, Any]) -> None:
216
+ def __init__(self, values: Mapping[Any, Any]) -> None:
218
217
  self._values = values
219
218
 
220
219
  def __eq__(self, target: object) -> bool:
@@ -235,7 +234,7 @@ class _DictMatching:
235
234
  return f"<DictMatching {self._values!r}>"
236
235
 
237
236
 
238
- def DictMatching(values: Mapping[str, Any]) -> Any:
237
+ def DictMatching(values: Mapping[Any, Any]) -> Any:
239
238
  """Match any dictionary with the passed in keys / values.
240
239
 
241
240
  Arguments:
@@ -5,13 +5,13 @@ from typing import List, Sequence
5
5
  from .errors import MissingRehearsalError
6
6
  from .spy_events import (
7
7
  AnySpyEvent,
8
+ PropAccessType,
9
+ PropRehearsal,
8
10
  SpyCall,
9
11
  SpyEvent,
10
12
  SpyPropAccess,
11
- WhenRehearsal,
12
13
  VerifyRehearsal,
13
- PropAccessType,
14
- PropRehearsal,
14
+ WhenRehearsal,
15
15
  )
16
16
 
17
17
 
@@ -33,10 +33,10 @@ class SpyLog:
33
33
  try:
34
34
  event = self._log[-1]
35
35
  except IndexError as e:
36
- raise MissingRehearsalError() from e
36
+ raise MissingRehearsalError.create() from e
37
37
 
38
38
  if not isinstance(event, SpyEvent):
39
- raise MissingRehearsalError()
39
+ raise MissingRehearsalError.create()
40
40
 
41
41
  spy, payload = _apply_ignore_extra_args(event, ignore_extra_args)
42
42
 
@@ -59,12 +59,12 @@ class SpyLog:
59
59
 
60
60
  while len(rehearsals) < count:
61
61
  if index < 0:
62
- raise MissingRehearsalError()
62
+ raise MissingRehearsalError.create()
63
63
 
64
64
  event = self._log[index]
65
65
 
66
66
  if not isinstance(event, (SpyEvent, PropRehearsal)):
67
- raise MissingRehearsalError()
67
+ raise MissingRehearsalError.create()
68
68
 
69
69
  if _is_verifiable(event):
70
70
  rehearsal = VerifyRehearsal(
@@ -82,7 +82,7 @@ class SpyLog:
82
82
  try:
83
83
  event = self._log[-1]
84
84
  except IndexError as e:
85
- raise MissingRehearsalError() from e
85
+ raise MissingRehearsalError.create() from e
86
86
 
87
87
  spy, payload = event
88
88
 
@@ -91,7 +91,7 @@ class SpyLog:
91
91
  or not isinstance(payload, SpyPropAccess)
92
92
  or payload.access_type != PropAccessType.GET
93
93
  ):
94
- raise MissingRehearsalError()
94
+ raise MissingRehearsalError.create()
95
95
 
96
96
  rehearsal = PropRehearsal(spy, payload)
97
97
  self._log[-1] = rehearsal
@@ -2,8 +2,8 @@
2
2
 
3
3
  from typing import Optional, Sequence
4
4
 
5
- from .spy_events import SpyEvent, VerifyRehearsal, match_event
6
5
  from .errors import VerifyError
6
+ from .spy_events import SpyEvent, VerifyRehearsal, match_event
7
7
 
8
8
  # ensure decoy.verifier does not pollute Pytest tracebacks
9
9
  __tracebackhide__ = True
@@ -42,7 +42,7 @@ class Verifier:
42
42
  calls_verified = match_count != 0 if times is None else match_count == times
43
43
 
44
44
  if not calls_verified:
45
- raise VerifyError(
45
+ raise VerifyError.create(
46
46
  rehearsals=rehearsals,
47
47
  calls=calls,
48
48
  times=times,
@@ -9,9 +9,9 @@ from .spy_events import (
9
9
  AnySpyEvent,
10
10
  SpyCall,
11
11
  SpyEvent,
12
+ SpyRehearsal,
12
13
  VerifyRehearsal,
13
14
  WhenRehearsal,
14
- SpyRehearsal,
15
15
  match_event,
16
16
  )
17
17
  from .warnings import DecoyWarning, MiscalledStubWarning, RedundantVerifyWarning
@@ -78,7 +78,7 @@ def _check_no_miscalled_stubs(all_events: Sequence[AnySpyEvent]) -> None:
78
78
 
79
79
  if is_stubbed and all(len(c.matching_rehearsals) == 0 for c in calls):
80
80
  _warn(
81
- MiscalledStubWarning(
81
+ MiscalledStubWarning.create(
82
82
  calls=[c.event for c in calls],
83
83
  rehearsals=rehearsals,
84
84
  )
@@ -90,8 +90,8 @@ def _check_no_redundant_verify(all_calls: Sequence[AnySpyEvent]) -> None:
90
90
  verify_rehearsals = [c for c in all_calls if isinstance(c, VerifyRehearsal)]
91
91
 
92
92
  for vr in verify_rehearsals:
93
- if any(wr for wr in when_rehearsals if wr == vr):
94
- _warn(RedundantVerifyWarning(rehearsal=vr))
93
+ if any(wr for wr in when_rehearsals if wr == vr): # type: ignore[comparison-overlap]
94
+ _warn(RedundantVerifyWarning.create(rehearsal=vr))
95
95
 
96
96
 
97
97
  def _warn(warning: DecoyWarning) -> None:
@@ -9,7 +9,7 @@ import os
9
9
  from typing import Sequence
10
10
 
11
11
  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
13
13
 
14
14
 
15
15
  class DecoyWarning(UserWarning):
@@ -31,18 +31,20 @@ class MiscalledStubWarning(DecoyWarning):
31
31
  [MiscalledStubWarning guide]: usage/errors-and-warnings.md#miscalledstubwarning
32
32
 
33
33
  Attributes:
34
- rehearsals: The mocks's configured rehearsals.
34
+ rehearsals: The mock's configured rehearsals.
35
35
  calls: Actual calls to the mock.
36
36
  """
37
37
 
38
38
  rehearsals: Sequence[SpyRehearsal]
39
39
  calls: Sequence[SpyEvent]
40
40
 
41
- def __init__(
42
- self,
41
+ @classmethod
42
+ def create(
43
+ cls,
43
44
  rehearsals: Sequence[SpyRehearsal],
44
45
  calls: Sequence[SpyEvent],
45
- ) -> None:
46
+ ) -> "MiscalledStubWarning":
47
+ """Create a MiscalledStubWarning."""
46
48
  heading = os.linesep.join(
47
49
  [
48
50
  "Stub was called but no matching rehearsal found.",
@@ -56,9 +58,11 @@ class MiscalledStubWarning(DecoyWarning):
56
58
  calls=calls,
57
59
  )
58
60
 
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
62
66
 
63
67
 
64
68
  class RedundantVerifyWarning(DecoyWarning):
@@ -74,7 +78,11 @@ class RedundantVerifyWarning(DecoyWarning):
74
78
  [RedundantVerifyWarning guide]: usage/errors-and-warnings.md#redundantverifywarning
75
79
  """
76
80
 
77
- def __init__(self, rehearsal: VerifyRehearsal) -> None:
81
+ rehearsal: VerifyRehearsal
82
+
83
+ @classmethod
84
+ def create(cls, rehearsal: VerifyRehearsal) -> "RedundantVerifyWarning":
85
+ """Create a RedundantVerifyWarning."""
78
86
  message = os.linesep.join(
79
87
  [
80
88
  "The same rehearsal was used in both a `when` and a `verify`.",
@@ -83,8 +91,11 @@ class RedundantVerifyWarning(DecoyWarning):
83
91
  "See https://michael.cousins.io/decoy/usage/errors-and-warnings/#redundantverifywarning",
84
92
  ]
85
93
  )
86
- super().__init__(message)
87
- self.rehearsal = rehearsal
94
+
95
+ result = cls(message)
96
+ result.rehearsal = rehearsal
97
+
98
+ return result
88
99
 
89
100
 
90
101
  class IncorrectCallWarning(DecoyWarning):
@@ -0,0 +1,115 @@
1
+ [project]
2
+ name = "decoy"
3
+ version = "2.2.2"
4
+ description = "Opinionated mocking library for Python"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.7"
8
+ authors = [
9
+ {name = "Michael Cousins", email = "michael@cousins.io>"},
10
+ ]
11
+ classifiers = [
12
+ "Development Status :: 5 - Production/Stable",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: OS Independent",
15
+ "Topic :: Software Development :: Testing",
16
+ "Topic :: Software Development :: Testing :: Mocking",
17
+ "Typing :: Typed",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://michael.cousins.io/decoy/"
22
+ Documentation = "https://michael.cousins.io/decoy/"
23
+ Repository = "https://github.com/mcous/decoy"
24
+ Issues = "https://github.com/mcous/decoy/issues"
25
+ Changelog = "https://github.com/mcous/decoy/releases"
26
+
27
+ [project.entry-points."pytest11"]
28
+ "decoy" = "decoy.pytest_plugin"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "poethepoet (==0.19.0) ; python_version < '3.8'",
33
+ "poethepoet (==0.30.0) ; python_version >= '3.8' and python_version < '3.9'",
34
+ "poethepoet (==0.35.0) ; python_version >= '3.9'",
35
+ ]
36
+
37
+ test = [
38
+ "coverage[toml] (==7.2.7) ; python_version < '3.8'",
39
+ "coverage[toml] (==7.6.1) ; python_version >= '3.8' and python_version < '3.9'",
40
+ "coverage[toml] (==7.9.1) ; python_version >= '3.9'",
41
+ "mypy (==1.4.1) ; python_version < '3.8'",
42
+ "mypy (==1.14.1) ; python_version >= '3.8' and python_version < '3.9'",
43
+ "mypy (==1.16.0) ; python_version >= '3.9'",
44
+ "pytest (==7.4.4) ; python_version < '3.8'",
45
+ "pytest (==8.3.5) ; python_version >= '3.8' and python_version < '3.9'",
46
+ "pytest (==8.4.0) ; python_version >= '3.9'",
47
+ "pytest-asyncio (==0.21.2) ; python_version < '3.8'",
48
+ "pytest-asyncio (==0.24.0) ; python_version >= '3.8' and python_version < '3.9'",
49
+ "pytest-asyncio (==1.0.0) ; python_version >= '3.9'",
50
+ "pytest-mypy-plugins (==2.0.0) ; python_version < '3.8'",
51
+ "pytest-mypy-plugins (==3.1.2) ; python_version >= '3.8' and python_version < '3.9'",
52
+ "pytest-mypy-plugins (==3.2.0) ; python_version >= '3.9'",
53
+ "pytest-xdist (==3.5.0) ; python_version < '3.8'",
54
+ "pytest-xdist (==3.6.1) ; python_version >= '3.8' and python_version < '3.9'",
55
+ "pytest-xdist (==3.7.0) ; python_version >= '3.9'",
56
+ "ruff (==0.14.7)"
57
+ ]
58
+
59
+ docs = [
60
+ "mkdocs (==1.6.1) ; python_version >= '3.10'",
61
+ "mkdocs-material (==9.6.21) ; python_version >= '3.10'",
62
+ "mkdocstrings[python] (==0.30.1) ; python_version >= '3.10'"
63
+ ]
64
+
65
+ [tool.poe.tasks]
66
+ all = [
67
+ "check",
68
+ "lint",
69
+ "format-check",
70
+ "test-once",
71
+ "coverage",
72
+ "build-docs",
73
+ "build-package",
74
+ ]
75
+ check = "mypy"
76
+ lint = "ruff check ."
77
+ format = "ruff format ."
78
+ format-check = "ruff format --check ."
79
+ test = "pytest -f"
80
+ test-once = "coverage run --branch --source=decoy -m pytest --mypy-same-process"
81
+ coverage = "coverage report"
82
+ coverage-xml = "coverage xml"
83
+ docs = "mkdocs serve"
84
+ build-docs = "mkdocs build"
85
+ build-package = "poetry build"
86
+ check-ci = ["check", "lint", "format-check"]
87
+ test-ci = ["test-once", "coverage-xml"]
88
+
89
+ [tool.pytest.ini_options]
90
+ addopts = "--color=yes --mypy-ini-file=tests/typing/mypy.ini --mypy-only-local-stub"
91
+ asyncio_mode = "auto"
92
+
93
+ [tool.mypy]
94
+ files = ["decoy", "tests"]
95
+ plugins = ["decoy/mypy/plugin.py"]
96
+ strict = true
97
+ show_error_codes = true
98
+
99
+ [tool.coverage.report]
100
+ exclude_lines = ["@overload", "if TYPE_CHECKING:"]
101
+
102
+ [tool.ruff]
103
+ target-version = "py37"
104
+ extend-exclude = [".cache"]
105
+
106
+ [tool.ruff.lint]
107
+ select = ["ANN", "B", "D", "E", "F", "RUF", "W"]
108
+ ignore = ["ANN401", "D107", "E501"]
109
+
110
+ [tool.ruff.lint.pydocstyle]
111
+ convention = "google"
112
+
113
+ [build-system]
114
+ requires = ["poetry_core>=1.0.0"]
115
+ build-backend = "poetry.core.masonry.api"
@@ -1,101 +0,0 @@
1
- [tool.poetry]
2
- name = "decoy"
3
- version = "2.2.0"
4
- description = "Opinionated mocking library for Python"
5
- authors = ["Michael Cousins <michael@cousins.io>"]
6
- license = "MIT"
7
- readme = "README.md"
8
- repository = "https://github.com/mcous/decoy"
9
- homepage = "https://michael.cousins.io/decoy/"
10
- documentation = "https://michael.cousins.io/decoy/"
11
-
12
- classifiers = [
13
- "Development Status :: 5 - Production/Stable",
14
- "Intended Audience :: Developers",
15
- "Operating System :: OS Independent",
16
- "Topic :: Software Development :: Testing",
17
- "Topic :: Software Development :: Testing :: Mocking",
18
- "Typing :: Typed",
19
- ]
20
-
21
- [tool.poetry.urls]
22
- "Changelog" = "https://github.com/mcous/decoy/releases"
23
-
24
- [tool.poetry.dependencies]
25
- python = "^3.7"
26
-
27
- [tool.poetry.group.dev.dependencies]
28
- poethepoet = "0.19.0"
29
-
30
- [tool.poetry.group.test.dependencies]
31
- coverage = { version = "7.2.7", extras = ["toml"] }
32
- mypy = [
33
- { version = "1.4.1", python = "<3.8" },
34
- { version = "1.5.0", python = ">=3.8" },
35
- ]
36
- pytest = "7.4.0"
37
- pytest-asyncio = "0.21.1"
38
- pytest-mypy-plugins = "2.0.0"
39
- pytest-xdist = "3.5.0"
40
- ruff = "0.11.0"
41
-
42
- [tool.poetry.group.docs.dependencies]
43
- mkdocs = { version = "1.5.3", python = ">=3.8" }
44
- mkdocs-material = { version = "9.4.8", python = ">=3.8" }
45
- mkdocstrings = { version = "0.23.0", extras = ["python"], python = ">=3.8" }
46
-
47
- [tool.poe.tasks]
48
- all = [
49
- "check",
50
- "lint",
51
- "format-check",
52
- "test-once",
53
- "coverage",
54
- "docs-build",
55
- "build",
56
- ]
57
- check = "mypy"
58
- lint = "ruff check ."
59
- format = "ruff format ."
60
- format-check = "ruff format --check ."
61
- test = "pytest -f"
62
- test-once = "coverage run --branch --source=decoy -m pytest --mypy-same-process"
63
- coverage = "coverage report"
64
- coverage-xml = "coverage xml"
65
- docs = "mkdocs serve"
66
- docs-build = "mkdocs build"
67
- build = "poetry build"
68
- check-ci = ["check", "lint", "format-check"]
69
- test-ci = ["test-once", "coverage-xml"]
70
- build-ci = ["docs-build", "build"]
71
-
72
- [tool.poetry.plugins."pytest11"]
73
- "decoy" = "decoy.pytest_plugin"
74
-
75
- [tool.pytest.ini_options]
76
- addopts = "--color=yes --mypy-ini-file=tests/typing/mypy.ini --mypy-only-local-stub"
77
- asyncio_mode = "auto"
78
-
79
- [tool.mypy]
80
- files = ["decoy", "tests"]
81
- plugins = ["decoy/mypy/plugin.py"]
82
- strict = true
83
- show_error_codes = true
84
-
85
- [tool.coverage.report]
86
- exclude_lines = ["@overload", "if TYPE_CHECKING:"]
87
-
88
- [tool.ruff]
89
- target-version = "py37"
90
- extend-exclude = [".cache"]
91
-
92
- [tool.ruff.lint]
93
- select = ["ANN", "B", "D", "E", "F", "RUF", "W"]
94
- ignore = ["ANN401", "D107", "E501"]
95
-
96
- [tool.ruff.lint.pydocstyle]
97
- convention = "google"
98
-
99
- [build-system]
100
- requires = ["poetry_core>=1.0.0"]
101
- build-backend = "poetry.core.masonry.api"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes