declarative-mocks 0.1.0__tar.gz → 0.2.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.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: declarative-mocks
3
+ Version: 0.2.0
4
+ Summary: Declare mock behavior as ordered expectations. Simple, typed, gomock-inspired DSL over unittest.mock
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: mock,mocking,testing,unittest,pytest
8
+ Author: Dimitry Churilov
9
+ Author-email: d.mdx3r@gmail.com
10
+ Requires-Python: >=3.11
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Classifier: Topic :: Software Development :: Testing :: Mocking
21
+ Classifier: Typing :: Typed
22
+ Project-URL: Homepage, https://github.com/MDx3R/declarative-mocks
23
+ Project-URL: Issues, https://github.com/MDx3R/declarative-mocks/issues
24
+ Project-URL: Repository, https://github.com/MDx3R/declarative-mocks
25
+ Description-Content-Type: text/markdown
26
+
27
+ ![Checks](https://github.com/MDx3R/declarative-mocks/actions/workflows/ci.yml/badge.svg)
28
+ ![PyPI](https://img.shields.io/pypi/v/declarative-mocks.svg)
29
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue.svg)
30
+ ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)
31
+
32
+ # declarative-mocks
33
+
34
+ **Declare mock behavior as ordered expectations**. Write readable and maintainable tests with explicit call patterns, return sequences, and side effects using **dmock**.
35
+
36
+ It is fully documented. Public methods and matchers ship with docstring examples, and pretty much everything about API and behavior is described in [REFERENCE.md](REFERENCE.md) and [SPEC.md](SPEC.md).
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install declarative-mocks
42
+ ```
43
+
44
+ _dmock_ requires Python 3.11+.
45
+
46
+ ## Usage
47
+
48
+ Normally you would set a return value, then check the object field by field and assert the call separately:
49
+
50
+ ```python
51
+ from dataclasses import dataclass
52
+ from unittest.mock import Mock
53
+
54
+ @dataclass
55
+ class User:
56
+ id: int
57
+ name: str
58
+ email: str
59
+
60
+ class Directory:
61
+ def lookup(self, name: str) -> User: ...
62
+
63
+ service = Mock(spec=Directory)
64
+ service.lookup.return_value = User(id=1, name="Ada", email="ada@example.com")
65
+
66
+ user = service.lookup("ada")
67
+ assert isinstance(user.id, int)
68
+ assert user.name.startswith("A")
69
+ assert "@" in user.email
70
+ service.lookup.assert_called_once_with("ada")
71
+ ```
72
+
73
+ Here's an example of how _dmock_ improves your tests:
74
+
75
+ ```python
76
+ from dmock import AnythingOfType, Contains, DeclarativeMock, StartsWith
77
+
78
+ service = DeclarativeMock(Directory)
79
+ service.expect("lookup", "ada").returns(User(id=1, name="Ada", email="ada@example.com"))
80
+
81
+ assert service.lookup("ada") == User(
82
+ id=AnythingOfType(int),
83
+ name=StartsWith("A"),
84
+ email=Contains("@"),
85
+ )
86
+ service.verify()
87
+ ```
88
+
89
+ That's not all! _dmock_ can do so much more than that, for example:
90
+
91
+ Successive outcomes apply to the next matching call, in order:
92
+
93
+ ```python
94
+ from dmock import DeclarativeMock
95
+
96
+ api = DeclarativeMock(ApiClient)
97
+ api.expect("next_page").returns({"items": [1], "cursor": "a"})
98
+ api.expect("next_page").returns({"items": [2], "cursor": None})
99
+ # first call → first dict; second call → second dict
100
+
101
+ calc = DeclarativeMock(Calculator)
102
+ calc.expect("price", 100).runs(lambda x: x - 1).returns(99)
103
+ assert calc.price(100) == 99 # from the lambda
104
+ assert calc.price(100) == 99 # from .returns(99)
105
+ ```
106
+
107
+ `expect(...)` with no further arguments matches a call with no arguments. A keyword matches only that keyword:
108
+
109
+ ```python
110
+ from dmock import Anything, DeclarativeMock
111
+
112
+ m = DeclarativeMock(Worker)
113
+ m.expect("run") # called with no arguments
114
+ m.expect("run", Anything()).returns(0) # any single positional arg
115
+ m.expect("run", job_id=123).returns("queued") # this keyword only
116
+ ```
117
+
118
+ `ANY_ARGS` and `ANY_KWARGS` match any positional and keyword arguments, including none:
119
+
120
+ ```python
121
+ from dmock import ANY_ARGS, ANY_KWARGS, DeclarativeMock
122
+
123
+ m = DeclarativeMock(Worker)
124
+ m.expect("run", ANY_ARGS, ANY_KWARGS).returns(0) # any positional and keyword args, including none
125
+ ```
126
+
127
+ Only one quantifier per expectation is allowed. The last outcome repeats when the count allows more calls, and `verify()` checks the counts:
128
+
129
+ ```python
130
+ from dmock import DeclarativeMock
131
+
132
+ calc = DeclarativeMock(Calculator)
133
+ calc.expect("price", 100).returns(99).once()
134
+ assert calc.price(100) == 99
135
+
136
+ calc.expect("price", 200).returns(99).times(3) # last outcome repeats for the extra calls
137
+ assert [calc.price(200) for _ in range(3)] == [99, 99, 99]
138
+
139
+ calc.expect("price", 300).returns(99).between(1, 3)
140
+ assert calc.price(300) == 99
141
+
142
+ calc.expect("price", 400).never()
143
+ calc.verify()
144
+ ```
145
+
146
+ Async methods use the same DSL. `runs()` accepts a sync or an async functions:
147
+
148
+ ```python
149
+ from dmock import Anything, DeclarativeMock
150
+
151
+ mock = DeclarativeMock(MyService)
152
+ mock.expect("aprocess_order", Anything()).returns("ok")
153
+ assert await mock.aprocess_order(1) == "ok"
154
+
155
+ mock.expect("aprocess_order", Anything()).runs(lambda order_id: f"sync-{order_id}")
156
+ assert await mock.aprocess_order(7) == "sync-7"
157
+
158
+
159
+ async def async_fn(order_id: int) -> str:
160
+ return f"async-{order_id}"
161
+
162
+
163
+ mock.expect("aprocess_order", Anything()).runs(async_fn)
164
+ assert await mock.aprocess_order(9) == "async-9"
165
+ ```
166
+
167
+ **dmock** is built on `unittest.mock`, has zero runtime dependencies, and doesn't patch anything. Unregistered calls fail immediately. Nothing on live modules or instances is rewritten.
168
+
169
+ Look up the dmock source code and tests yourself, it's full of examples!
170
+
171
+ ## Limitations & non-goals
172
+
173
+ - **No signature binding.** `expect("method", …)` checks that `method` exists on the spec, not that the argument list matches the real signature.
174
+ - **No automatic** `verify()`. You call `verify()` yourself (a pytest hook is on the roadmap).
175
+ - **No spies and no patching.** The library does not wrap live objects, modules, or `sys.modules`. Just construct a `DeclarativeMock` and pass it in.
176
+ - **Not thread-safe.** Concurrent use of one mock from multiple threads is out of scope.
177
+
178
+ ## Roadmap
179
+
180
+ After 0.2.0, in no particular order:
181
+
182
+ - **pytest auto-**`verify()` - a plugin or fixture that runs `verify()` at the end of each test (gomock-style `Finish` on teardown), so you do not have to remember the call.
183
+ - **Per-outcome counts (maybe)** - repeat a `.returns()` / `.raises()` / `.runs()` without chaining it n times (today `.returns(1).returns(1).returns(1).returns(2)`).
184
+
185
+ Other ideas belong in GitHub Issues.
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ poetry install
191
+ ruff check .
192
+ ruff format --check .
193
+ mypy src tests
194
+ pytest
195
+ pytest --cov --cov-report=term-missing
196
+ ```
197
+
198
+ See **[CONTRIBUTING.md](CONTRIBUTING.md)** for setup, commits, and tests. Agents follow **[AGENTS.md](AGENTS.md)**.
199
+
200
+ ## License
201
+
202
+ MIT
203
+
@@ -0,0 +1,176 @@
1
+ ![Checks](https://github.com/MDx3R/declarative-mocks/actions/workflows/ci.yml/badge.svg)
2
+ ![PyPI](https://img.shields.io/pypi/v/declarative-mocks.svg)
3
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue.svg)
4
+ ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)
5
+
6
+ # declarative-mocks
7
+
8
+ **Declare mock behavior as ordered expectations**. Write readable and maintainable tests with explicit call patterns, return sequences, and side effects using **dmock**.
9
+
10
+ It is fully documented. Public methods and matchers ship with docstring examples, and pretty much everything about API and behavior is described in [REFERENCE.md](REFERENCE.md) and [SPEC.md](SPEC.md).
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install declarative-mocks
16
+ ```
17
+
18
+ _dmock_ requires Python 3.11+.
19
+
20
+ ## Usage
21
+
22
+ Normally you would set a return value, then check the object field by field and assert the call separately:
23
+
24
+ ```python
25
+ from dataclasses import dataclass
26
+ from unittest.mock import Mock
27
+
28
+ @dataclass
29
+ class User:
30
+ id: int
31
+ name: str
32
+ email: str
33
+
34
+ class Directory:
35
+ def lookup(self, name: str) -> User: ...
36
+
37
+ service = Mock(spec=Directory)
38
+ service.lookup.return_value = User(id=1, name="Ada", email="ada@example.com")
39
+
40
+ user = service.lookup("ada")
41
+ assert isinstance(user.id, int)
42
+ assert user.name.startswith("A")
43
+ assert "@" in user.email
44
+ service.lookup.assert_called_once_with("ada")
45
+ ```
46
+
47
+ Here's an example of how _dmock_ improves your tests:
48
+
49
+ ```python
50
+ from dmock import AnythingOfType, Contains, DeclarativeMock, StartsWith
51
+
52
+ service = DeclarativeMock(Directory)
53
+ service.expect("lookup", "ada").returns(User(id=1, name="Ada", email="ada@example.com"))
54
+
55
+ assert service.lookup("ada") == User(
56
+ id=AnythingOfType(int),
57
+ name=StartsWith("A"),
58
+ email=Contains("@"),
59
+ )
60
+ service.verify()
61
+ ```
62
+
63
+ That's not all! _dmock_ can do so much more than that, for example:
64
+
65
+ Successive outcomes apply to the next matching call, in order:
66
+
67
+ ```python
68
+ from dmock import DeclarativeMock
69
+
70
+ api = DeclarativeMock(ApiClient)
71
+ api.expect("next_page").returns({"items": [1], "cursor": "a"})
72
+ api.expect("next_page").returns({"items": [2], "cursor": None})
73
+ # first call → first dict; second call → second dict
74
+
75
+ calc = DeclarativeMock(Calculator)
76
+ calc.expect("price", 100).runs(lambda x: x - 1).returns(99)
77
+ assert calc.price(100) == 99 # from the lambda
78
+ assert calc.price(100) == 99 # from .returns(99)
79
+ ```
80
+
81
+ `expect(...)` with no further arguments matches a call with no arguments. A keyword matches only that keyword:
82
+
83
+ ```python
84
+ from dmock import Anything, DeclarativeMock
85
+
86
+ m = DeclarativeMock(Worker)
87
+ m.expect("run") # called with no arguments
88
+ m.expect("run", Anything()).returns(0) # any single positional arg
89
+ m.expect("run", job_id=123).returns("queued") # this keyword only
90
+ ```
91
+
92
+ `ANY_ARGS` and `ANY_KWARGS` match any positional and keyword arguments, including none:
93
+
94
+ ```python
95
+ from dmock import ANY_ARGS, ANY_KWARGS, DeclarativeMock
96
+
97
+ m = DeclarativeMock(Worker)
98
+ m.expect("run", ANY_ARGS, ANY_KWARGS).returns(0) # any positional and keyword args, including none
99
+ ```
100
+
101
+ Only one quantifier per expectation is allowed. The last outcome repeats when the count allows more calls, and `verify()` checks the counts:
102
+
103
+ ```python
104
+ from dmock import DeclarativeMock
105
+
106
+ calc = DeclarativeMock(Calculator)
107
+ calc.expect("price", 100).returns(99).once()
108
+ assert calc.price(100) == 99
109
+
110
+ calc.expect("price", 200).returns(99).times(3) # last outcome repeats for the extra calls
111
+ assert [calc.price(200) for _ in range(3)] == [99, 99, 99]
112
+
113
+ calc.expect("price", 300).returns(99).between(1, 3)
114
+ assert calc.price(300) == 99
115
+
116
+ calc.expect("price", 400).never()
117
+ calc.verify()
118
+ ```
119
+
120
+ Async methods use the same DSL. `runs()` accepts a sync or an async functions:
121
+
122
+ ```python
123
+ from dmock import Anything, DeclarativeMock
124
+
125
+ mock = DeclarativeMock(MyService)
126
+ mock.expect("aprocess_order", Anything()).returns("ok")
127
+ assert await mock.aprocess_order(1) == "ok"
128
+
129
+ mock.expect("aprocess_order", Anything()).runs(lambda order_id: f"sync-{order_id}")
130
+ assert await mock.aprocess_order(7) == "sync-7"
131
+
132
+
133
+ async def async_fn(order_id: int) -> str:
134
+ return f"async-{order_id}"
135
+
136
+
137
+ mock.expect("aprocess_order", Anything()).runs(async_fn)
138
+ assert await mock.aprocess_order(9) == "async-9"
139
+ ```
140
+
141
+ **dmock** is built on `unittest.mock`, has zero runtime dependencies, and doesn't patch anything. Unregistered calls fail immediately. Nothing on live modules or instances is rewritten.
142
+
143
+ Look up the dmock source code and tests yourself, it's full of examples!
144
+
145
+ ## Limitations & non-goals
146
+
147
+ - **No signature binding.** `expect("method", …)` checks that `method` exists on the spec, not that the argument list matches the real signature.
148
+ - **No automatic** `verify()`. You call `verify()` yourself (a pytest hook is on the roadmap).
149
+ - **No spies and no patching.** The library does not wrap live objects, modules, or `sys.modules`. Just construct a `DeclarativeMock` and pass it in.
150
+ - **Not thread-safe.** Concurrent use of one mock from multiple threads is out of scope.
151
+
152
+ ## Roadmap
153
+
154
+ After 0.2.0, in no particular order:
155
+
156
+ - **pytest auto-**`verify()` - a plugin or fixture that runs `verify()` at the end of each test (gomock-style `Finish` on teardown), so you do not have to remember the call.
157
+ - **Per-outcome counts (maybe)** - repeat a `.returns()` / `.raises()` / `.runs()` without chaining it n times (today `.returns(1).returns(1).returns(1).returns(2)`).
158
+
159
+ Other ideas belong in GitHub Issues.
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ poetry install
165
+ ruff check .
166
+ ruff format --check .
167
+ mypy src tests
168
+ pytest
169
+ pytest --cov --cov-report=term-missing
170
+ ```
171
+
172
+ See **[CONTRIBUTING.md](CONTRIBUTING.md)** for setup, commits, and tests. Agents follow **[AGENTS.md](AGENTS.md)**.
173
+
174
+ ## License
175
+
176
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "declarative-mocks"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Declare mock behavior as ordered expectations. Simple, typed, gomock-inspired DSL over unittest.mock"
5
5
  authors = [{ name = "Dimitry Churilov", email = "d.mdx3r@gmail.com" }]
6
6
  license = "MIT"
@@ -14,10 +14,41 @@ from dmock._expectation import Expectation, in_order
14
14
  from dmock._matchers import (
15
15
  ANY_ARGS,
16
16
  ANY_KWARGS,
17
+ AWARE_DATETIME,
18
+ EMPTY,
19
+ FALSY,
20
+ NAIVE_DATETIME,
21
+ TRUTHY,
22
+ And,
17
23
  Anything,
18
24
  AnythingOfType,
25
+ Approx,
26
+ Args,
27
+ Attr,
28
+ Contains,
29
+ Empty,
30
+ EndsWith,
31
+ Equals,
32
+ GreaterEquals,
33
+ GreaterThan,
34
+ In,
35
+ Includes,
36
+ Is,
37
+ LessEquals,
38
+ LessThan,
19
39
  MatchedBy,
20
40
  Matcher,
41
+ Maybe,
42
+ Never,
43
+ Not,
44
+ Nothing,
45
+ Or,
46
+ Range,
47
+ Regex,
48
+ RegexFullmatch,
49
+ RegexMatch,
50
+ RegexSearch,
51
+ StartsWith,
21
52
  )
22
53
  from dmock._mock import DeclarativeMock
23
54
 
@@ -31,14 +62,45 @@ except PackageNotFoundError: # pragma: no cover
31
62
  __all__ = [
32
63
  "ANY_ARGS",
33
64
  "ANY_KWARGS",
65
+ "AWARE_DATETIME",
66
+ "EMPTY",
67
+ "FALSY",
68
+ "NAIVE_DATETIME",
69
+ "TRUTHY",
70
+ "And",
34
71
  "Anything",
35
72
  "AnythingOfType",
73
+ "Approx",
74
+ "Args",
75
+ "Attr",
36
76
  "ConfigurationError",
77
+ "Contains",
37
78
  "DeclarativeMock",
38
79
  "DeclarativeMockError",
80
+ "Empty",
81
+ "EndsWith",
82
+ "Equals",
39
83
  "Expectation",
84
+ "GreaterEquals",
85
+ "GreaterThan",
86
+ "In",
87
+ "Includes",
88
+ "Is",
89
+ "LessEquals",
90
+ "LessThan",
40
91
  "MatchedBy",
41
92
  "Matcher",
93
+ "Maybe",
94
+ "Never",
95
+ "Not",
96
+ "Nothing",
97
+ "Or",
98
+ "Range",
99
+ "Regex",
100
+ "RegexFullmatch",
101
+ "RegexMatch",
102
+ "RegexSearch",
103
+ "StartsWith",
42
104
  "UnexpectedCallError",
43
105
  "UnsatisfiedExpectationError",
44
106
  "in_order",