deep-mock 0.1.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,539 @@
1
+ Metadata-Version: 2.4
2
+ Name: deep-mock
3
+ Version: 0.1.0
4
+ Summary: A Python mocking library
5
+ Author-email: Ogi Zmaj Dzedaj <ognjen.bozickovic@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/styoe/deep-mock
8
+ Project-URL: Repository, https://github.com/styoe/deep-mock
9
+ Project-URL: Issues, https://github.com/styoe/deep-mock/issues
10
+ Keywords: mock,testing,unittest,mocking
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
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: Topic :: Software Development :: Testing
21
+ Classifier: Topic :: Software Development :: Testing :: Mocking
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
28
+ Requires-Dist: black>=23.0; extra == "dev"
29
+ Requires-Dist: isort>=5.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.1; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # deep-mock
35
+
36
+ A Python mocking library that simplifies patching and handles edge cases that `unittest.mock.patch` cannot solve.
37
+
38
+ ## The Problem
39
+
40
+ Python's standard `unittest.mock.patch` has limitations:
41
+
42
+ 1. **You must patch at the right location** - If `module_b` imports `func` from `module_a`, you need to patch `module_b.func`, not `module_a.func`. This gets complicated with multiple imports.
43
+
44
+ 2. **Module-level state is not recomputed** - If a module computes values at import time using the function you're mocking, those values remain stale:
45
+
46
+ ```python
47
+ # cache.py
48
+ from database import fetch_user
49
+
50
+ SYSTEM_USER = fetch_user("system") # Computed ONCE at import time
51
+ ```
52
+
53
+ Even if you patch `fetch_user`, `SYSTEM_USER` still has the original value.
54
+
55
+ 3. **Indirect dependencies are invisible** - If `module_c` imports from `module_b` which imports from `module_a`, patching `module_a` won't affect `module_c`'s module-level state.
56
+
57
+ ## The Solution
58
+
59
+ `deep-mock` solves all of these problems:
60
+
61
+ - **Patch once, apply everywhere** - Patches propagate to all modules that imported the mocked function
62
+ - **Auto-reload modules** - Module-level state is automatically recomputed with mocked values
63
+ - **Handle edge cases** - `import_and_reload_module` for indirect dependencies
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ pip install deep-mock
69
+ ```
70
+
71
+ ## Quick Start
72
+
73
+ ```python
74
+ from unittest.mock import Mock
75
+ from deep_mock import MockSysModules
76
+
77
+ mock_fetch = Mock(return_value={"id": "1", "name": "Test User"})
78
+
79
+ with MockSysModules([
80
+ ("myapp.database", "fetch_user", mock_fetch),
81
+ ]):
82
+ # All modules that import fetch_user now use the mock
83
+ # Module-level state is recomputed with the mock
84
+ from myapp.services import user_service
85
+ assert user_service.get_user("1")["name"] == "Test User"
86
+
87
+ # After exiting, everything is restored to original
88
+ ```
89
+
90
+ ## Examples
91
+
92
+ ### Example 1: Simple Patching
93
+
94
+ The most basic use case - mock a function and all its imports are automatically patched.
95
+
96
+ ```python
97
+ from unittest.mock import Mock
98
+ from deep_mock import MockSysModules
99
+
100
+ # Create your mock
101
+ mock_db_connect = Mock(return_value={"connected": True})
102
+
103
+ # Use MockSysModules context manager
104
+ with MockSysModules([
105
+ ("myapp.database", "connect", mock_db_connect),
106
+ ]):
107
+ from myapp.api import handler
108
+
109
+ result = handler.process_request()
110
+
111
+ # Assert the mock was called
112
+ mock_db_connect.assert_called_once()
113
+ ```
114
+
115
+ ### Example 2: Module-Level State (Direct Dependencies)
116
+
117
+ When a module computes values at import time, `deep-mock` automatically reloads it.
118
+
119
+ ```python
120
+ # myapp/cache.py
121
+ from myapp.database import fetch_user
122
+
123
+ # This runs ONCE at import time
124
+ SYSTEM_USER = fetch_user("system")
125
+
126
+ def get_system_user():
127
+ return SYSTEM_USER
128
+ ```
129
+
130
+ ```python
131
+ # test_cache.py
132
+ from unittest.mock import Mock
133
+ from deep_mock import MockSysModules
134
+
135
+ def test_system_user_is_mocked():
136
+ mock_fetch = Mock(return_value={"id": "system", "name": "Mock Admin"})
137
+
138
+ with MockSysModules([
139
+ ("myapp.database", "fetch_user", mock_fetch),
140
+ ]):
141
+ from myapp.cache import get_system_user
142
+
143
+ # SYSTEM_USER was recomputed with the mock!
144
+ assert get_system_user()["name"] == "Mock Admin"
145
+
146
+ # After exiting, SYSTEM_USER is restored to real value
147
+ ```
148
+
149
+ ### Example 3: Indirect Dependencies (Edge Case)
150
+
151
+ When module C depends on module B which depends on module A, and you mock something in A:
152
+
153
+ ```python
154
+ # myapp/database.py
155
+ def fetch_user(user_id):
156
+ return {"id": user_id, "name": "Real User"}
157
+
158
+ # myapp/cache.py
159
+ from myapp.database import fetch_user
160
+ SYSTEM_USER = fetch_user("system")
161
+
162
+ def get_system_user():
163
+ return SYSTEM_USER
164
+
165
+ # myapp/user_service.py
166
+ from myapp.cache import get_system_user
167
+
168
+ # Indirect dependency - imports from cache, not database
169
+ USER_GREETING = f"Hello, {get_system_user()['name']}!"
170
+ ```
171
+
172
+ ```python
173
+ # test_indirect.py
174
+ from unittest.mock import Mock
175
+ from deep_mock import MockSysModules, import_and_reload_module
176
+
177
+ def test_indirect_dependency():
178
+ mock_fetch = Mock(return_value={"id": "system", "name": "Mock User"})
179
+
180
+ # Import user_service BEFORE mocking
181
+ from myapp import user_service
182
+ assert user_service.USER_GREETING == "Hello, Real User!"
183
+
184
+ with MockSysModules([
185
+ ("myapp.database", "fetch_user", mock_fetch),
186
+ ]):
187
+ # cache is auto-reloaded (direct dependency)
188
+ from myapp.cache import get_system_user
189
+ assert get_system_user()["name"] == "Mock User"
190
+
191
+ # user_service is NOT auto-reloaded (indirect dependency)
192
+ # Its USER_GREETING still has the old value!
193
+ assert user_service.USER_GREETING == "Hello, Real User!"
194
+
195
+ # Use import_and_reload_module to fix this
196
+ user_service = import_and_reload_module("myapp.user_service")
197
+ assert user_service.USER_GREETING == "Hello, Mock User!"
198
+ ```
199
+
200
+ ### Example 4: Mocking Multiple Functions
201
+
202
+ ```python
203
+ from unittest.mock import Mock
204
+ from deep_mock import MockSysModules
205
+
206
+ mock_fetch = Mock(return_value={"id": "1", "name": "Test"})
207
+ mock_save = Mock(return_value=True)
208
+ mock_delete = Mock(return_value=True)
209
+
210
+ with MockSysModules([
211
+ ("myapp.database", "fetch_user", mock_fetch),
212
+ ("myapp.database", "save_user", mock_save),
213
+ ("myapp.database", "delete_user", mock_delete),
214
+ ]):
215
+ # All three functions are mocked everywhere
216
+ pass
217
+ ```
218
+
219
+ ### Example 5: Mocking Classes
220
+
221
+ ```python
222
+ from unittest.mock import Mock
223
+ from deep_mock import MockSysModules
224
+
225
+ # Create a mock class
226
+ MockDatabaseClient = Mock()
227
+ mock_instance = Mock()
228
+ mock_instance.connect.return_value = {"status": "connected"}
229
+ mock_instance.query.return_value = [{"id": 1}]
230
+ MockDatabaseClient.return_value = mock_instance
231
+
232
+ with MockSysModules([
233
+ ("myapp.database", "DatabaseClient", MockDatabaseClient),
234
+ ]):
235
+ from myapp.services import data_service
236
+
237
+ result = data_service.get_all_records()
238
+ MockDatabaseClient.assert_called_once()
239
+ ```
240
+
241
+ ## Configuration with conftest.py
242
+
243
+ Set project-wide defaults in your `conftest.py`:
244
+
245
+ ```python
246
+ # conftest.py
247
+ from deep_mock import DeepMockConfig
248
+
249
+ def pytest_configure(config):
250
+ DeepMockConfig.configure(
251
+ base_dir="src", # Base directory to scan for imports
252
+ allowed_dirs=["src/myapp"], # Only scan these directories
253
+ )
254
+ ```
255
+
256
+ Now all `MockSysModules` usage will use these defaults:
257
+
258
+ ```python
259
+ # test_something.py
260
+ from deep_mock import MockSysModules
261
+
262
+ # Uses conftest.py defaults automatically
263
+ with MockSysModules([("myapp.database", "fetch_user", mock)]):
264
+ pass
265
+
266
+ # Override for specific test if needed
267
+ with MockSysModules(
268
+ [("myapp.database", "fetch_user", mock)],
269
+ base_dir="other_dir",
270
+ ):
271
+ pass
272
+ ```
273
+
274
+ ## Debugging Mock Calls
275
+
276
+ Use the debugging utilities to inspect mock calls:
277
+
278
+ ```python
279
+ from unittest.mock import Mock
280
+ from deep_mock import MockSysModules, print_all_mock_calls, find_calls_in_mock_calls
281
+
282
+ mock_db = Mock()
283
+
284
+ with MockSysModules([("myapp.database", "db", mock_db)]):
285
+ from myapp.services import user_service
286
+ user_service.create_user({"name": "Alice"})
287
+ user_service.create_user({"name": "Bob"})
288
+
289
+ # Print all calls for debugging
290
+ print_all_mock_calls(mock_db)
291
+
292
+ # Find specific calls
293
+ save_calls = find_calls_in_mock_calls(
294
+ mock_db,
295
+ "save",
296
+ call_filter=lambda args, kwargs: args[0]["name"] == "Alice"
297
+ )
298
+ ```
299
+
300
+ ## API Reference
301
+
302
+ ### `MockSysModules`
303
+
304
+ Context manager for mocking with automatic module reloading.
305
+
306
+ ```python
307
+ class MockSysModules:
308
+ def __init__(
309
+ self,
310
+ override_modules: list[tuple[str, str, Any]] | None = None,
311
+ base_dir: str | None = None,
312
+ allowed_dirs: list[str] | None = None,
313
+ ):
314
+ """
315
+ Args:
316
+ override_modules: List of (module_name, attribute_name, mock) tuples.
317
+ - module_name: Full module path (e.g., "myapp.database")
318
+ - attribute_name: Name of the function/class to mock (e.g., "fetch_user")
319
+ - mock: The mock object to replace it with
320
+
321
+ base_dir: Base directory to scan for modules that import the mocked
322
+ attributes. Defaults to DeepMockConfig.base_dir or ".".
323
+
324
+ allowed_dirs: List of directories to limit scanning to. If None,
325
+ scans all directories under base_dir. Defaults to
326
+ DeepMockConfig.allowed_dirs.
327
+ """
328
+ ```
329
+
330
+ **Behavior:**
331
+
332
+ 1. **On enter (`__enter__`):**
333
+ - Patches the specified attributes in the source modules
334
+ - Finds all loaded modules that imported these attributes
335
+ - Patches those modules too
336
+ - Reloads all affected modules so module-level state is recomputed with mocks
337
+
338
+ 2. **On exit (`__exit__`):**
339
+ - Restores all original attributes
340
+ - Reloads all affected modules so module-level state is recomputed with real values
341
+ - Also reloads modules that were imported during the context
342
+
343
+ ---
344
+
345
+ ### `mock_sys_modules`
346
+
347
+ Function version of `MockSysModules`. Returns a cleanup function.
348
+
349
+ ```python
350
+ def mock_sys_modules(
351
+ override_modules: list[tuple[str, str, Any]] | None = None,
352
+ base_dir: str = ".",
353
+ allowed_dirs: list[str] | None = None,
354
+ ) -> Callable[[], None]:
355
+ """
356
+ Apply mocks and return a cleanup function.
357
+
358
+ Args:
359
+ override_modules: List of (module_name, attribute_name, mock) tuples.
360
+ base_dir: Base directory to scan for imports.
361
+ allowed_dirs: Directories to limit scanning to.
362
+
363
+ Returns:
364
+ A cleanup function that restores original values and reloads modules.
365
+
366
+ Example:
367
+ cleanup = mock_sys_modules([("myapp.db", "fetch", mock)])
368
+ try:
369
+ # ... test code ...
370
+ finally:
371
+ cleanup()
372
+ """
373
+ ```
374
+
375
+ ---
376
+
377
+ ### `import_and_reload_module`
378
+
379
+ Import a module, or reload it if already imported. Essential for handling indirect dependencies.
380
+
381
+ ```python
382
+ def import_and_reload_module(module_name: str) -> ModuleType:
383
+ """
384
+ Import or reload a module, returning the module object.
385
+
386
+ This is necessary for modules with INDIRECT dependencies on mocked functions.
387
+ These modules import from other modules (not directly from the mocked module),
388
+ so they are not automatically detected and reloaded by MockSysModules.
389
+
390
+ Args:
391
+ module_name: Full module path (e.g., "myapp.services.user_service")
392
+
393
+ Returns:
394
+ The imported/reloaded module object.
395
+
396
+ Example:
397
+ # user_service imports from cache, which imports from database
398
+ # When we mock database.fetch_user, user_service is not auto-reloaded
399
+
400
+ with MockSysModules([("myapp.database", "fetch_user", mock)]):
401
+ # Manually reload to recompute module-level state
402
+ user_service = import_and_reload_module("myapp.services.user_service")
403
+ assert user_service.CACHED_VALUE == "mocked value"
404
+ """
405
+ ```
406
+
407
+ **When to use:**
408
+
409
+ - Module has module-level state computed from an indirect dependency
410
+ - Module was imported before entering `MockSysModules` and has indirect dependencies
411
+ - You need to force a reload at a specific point in your test
412
+
413
+ ---
414
+
415
+ ### `DeepMockConfig`
416
+
417
+ Global configuration for `deep-mock` defaults. Configure once in `conftest.py`.
418
+
419
+ ```python
420
+ class DeepMockConfig:
421
+ base_dir: str = "."
422
+ allowed_dirs: list[str] | None = None
423
+
424
+ @classmethod
425
+ def configure(
426
+ cls,
427
+ base_dir: str | None = None,
428
+ allowed_dirs: list[str] | None = None,
429
+ ):
430
+ """
431
+ Set default values for MockSysModules.
432
+
433
+ Args:
434
+ base_dir: Default base directory for scanning modules.
435
+ allowed_dirs: Default directories to limit scanning to.
436
+
437
+ Example:
438
+ # In conftest.py
439
+ def pytest_configure(config):
440
+ DeepMockConfig.configure(
441
+ base_dir="src",
442
+ allowed_dirs=["src/myapp", "src/lib"],
443
+ )
444
+ """
445
+
446
+ @classmethod
447
+ def reset(cls):
448
+ """Reset configuration to defaults."""
449
+ ```
450
+
451
+ ---
452
+
453
+ ### `find_calls_in_mock_calls`
454
+
455
+ Filter mock call history by name and optional predicate.
456
+
457
+ ```python
458
+ def find_calls_in_mock_calls(
459
+ mock,
460
+ call_name: str,
461
+ call_filter: Callable[[tuple, dict[str, Any]], bool] | None = None,
462
+ ) -> list[tuple[str, tuple, dict]]:
463
+ """
464
+ Find specific calls in a mock's call history.
465
+
466
+ Args:
467
+ mock: The mock object to inspect.
468
+ call_name: Name of the method call to find (e.g., "save", "().query").
469
+ call_filter: Optional function (args, kwargs) -> bool to filter calls.
470
+
471
+ Returns:
472
+ List of (call_name, args, kwargs) tuples matching the criteria.
473
+
474
+ Example:
475
+ # Find all 'save' calls where the first arg has status='active'
476
+ calls = find_calls_in_mock_calls(
477
+ mock_db,
478
+ "save",
479
+ call_filter=lambda args, kwargs: args[0]["status"] == "active"
480
+ )
481
+ assert len(calls) == 2
482
+ """
483
+ ```
484
+
485
+ ---
486
+
487
+ ### `print_all_mock_calls`
488
+
489
+ Debug utility to print all calls made to a mock.
490
+
491
+ ```python
492
+ def print_all_mock_calls(mock):
493
+ """
494
+ Print all calls made to a mock object for debugging.
495
+
496
+ Prints each call with:
497
+ - Call name (e.g., "", "().method", "().method().chain")
498
+ - Call args (tuple)
499
+ - Call kwargs (dict)
500
+
501
+ Example output:
502
+ --------------------------------
503
+ Printing all mock calls
504
+ --------------------------------
505
+ Call name - type: <class 'str'> ().collection
506
+ Call args - type: <class 'tuple'> ('users',)
507
+ Call kwargs - type: <class 'dict'> {}
508
+ --------------------------------
509
+ """
510
+ ```
511
+
512
+ ---
513
+
514
+ ### `fake_useless_decorator`
515
+
516
+ A pass-through decorator for replacing real decorators in tests.
517
+
518
+ ```python
519
+ def fake_useless_decorator(func):
520
+ """
521
+ A decorator that does nothing - just returns the function as-is.
522
+
523
+ Useful for mocking decorators that have side effects you want to avoid
524
+ in tests (e.g., caching, authentication, rate limiting).
525
+
526
+ Example:
527
+ with MockSysModules([
528
+ ("myapp.decorators", "require_auth", fake_useless_decorator),
529
+ ("myapp.decorators", "cache_result", fake_useless_decorator),
530
+ ]):
531
+ # Decorators are now no-ops
532
+ from myapp.api import handler
533
+ handler.protected_endpoint() # No auth check
534
+ """
535
+ ```
536
+
537
+ ## License
538
+
539
+ MIT