Skip to main content

A Python mocking library

Project description

deep-mock

A Python mocking library that simplifies patching and handles edge cases that unittest.mock.patch cannot solve.

The Problem

Python's standard unittest.mock.patch has limitations:

  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.

  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:

# cache.py
from database import fetch_user

SYSTEM_USER = fetch_user("system")  # Computed ONCE at import time

Even if you patch fetch_user, SYSTEM_USER still has the original value.

  1. 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.

The Solution

deep-mock solves all of these problems:

  • Patch once, apply everywhere - Patches propagate to all modules that imported the mocked function
  • Auto-reload modules - Module-level state is automatically recomputed with mocked values
  • Handle edge cases - import_and_reload_module for indirect dependencies

Installation

pip install deep-mock

Quick Start

from unittest.mock import Mock
from deep_mock import MockSysModules

mock_fetch = Mock(return_value={"id": "1", "name": "Test User"})

with MockSysModules([
    ("myapp.database", "fetch_user", mock_fetch),
]):
    # All modules that import fetch_user now use the mock
    # Module-level state is recomputed with the mock
    from myapp.services import user_service
    assert user_service.get_user("1")["name"] == "Test User"

# After exiting, everything is restored to original

Examples

Example 1: Simple Patching

The most basic use case - mock a function and all its imports are automatically patched.

from unittest.mock import Mock
from deep_mock import MockSysModules

# Create your mock
mock_db_connect = Mock(return_value={"connected": True})

# Use MockSysModules context manager
with MockSysModules([
    ("myapp.database", "connect", mock_db_connect),
]):
    from myapp.api import handler

    result = handler.process_request()

    # Assert the mock was called
    mock_db_connect.assert_called_once()

Example 2: Module-Level State (Direct Dependencies)

When a module computes values at import time, deep-mock automatically reloads it.

# myapp/cache.py
from myapp.database import fetch_user

# This runs ONCE at import time
SYSTEM_USER = fetch_user("system")

def get_system_user():
    return SYSTEM_USER
# test_cache.py
from unittest.mock import Mock
from deep_mock import MockSysModules

def test_system_user_is_mocked():
    mock_fetch = Mock(return_value={"id": "system", "name": "Mock Admin"})

    with MockSysModules([
        ("myapp.database", "fetch_user", mock_fetch),
    ]):
        from myapp.cache import get_system_user

        # SYSTEM_USER was recomputed with the mock!
        assert get_system_user()["name"] == "Mock Admin"

    # After exiting, SYSTEM_USER is restored to real value

Example 3: Indirect Dependencies (Edge Case)

When module C depends on module B which depends on module A, and you mock something in A:

# myapp/database.py
def fetch_user(user_id):
    return {"id": user_id, "name": "Real User"}

# myapp/cache.py
from myapp.database import fetch_user
SYSTEM_USER = fetch_user("system")

def get_system_user():
    return SYSTEM_USER

# myapp/user_service.py
from myapp.cache import get_system_user

# Indirect dependency - imports from cache, not database
USER_GREETING = f"Hello, {get_system_user()['name']}!"
# test_indirect.py
from unittest.mock import Mock
from deep_mock import MockSysModules, import_and_reload_module

def test_indirect_dependency():
    mock_fetch = Mock(return_value={"id": "system", "name": "Mock User"})

    # Import user_service BEFORE mocking
    from myapp import user_service
    assert user_service.USER_GREETING == "Hello, Real User!"

    with MockSysModules([
        ("myapp.database", "fetch_user", mock_fetch),
    ]):
        # cache is auto-reloaded (direct dependency)
        from myapp.cache import get_system_user
        assert get_system_user()["name"] == "Mock User"

        # user_service is NOT auto-reloaded (indirect dependency)
        # Its USER_GREETING still has the old value!
        assert user_service.USER_GREETING == "Hello, Real User!"

        # Use import_and_reload_module to fix this
        user_service = import_and_reload_module("myapp.user_service")
        assert user_service.USER_GREETING == "Hello, Mock User!"

Example 4: Mocking Multiple Functions

from unittest.mock import Mock
from deep_mock import MockSysModules

mock_fetch = Mock(return_value={"id": "1", "name": "Test"})
mock_save = Mock(return_value=True)
mock_delete = Mock(return_value=True)

with MockSysModules([
    ("myapp.database", "fetch_user", mock_fetch),
    ("myapp.database", "save_user", mock_save),
    ("myapp.database", "delete_user", mock_delete),
]):
    # All three functions are mocked everywhere
    pass

Example 5: Mocking Classes

from unittest.mock import Mock
from deep_mock import MockSysModules

# Create a mock class
MockDatabaseClient = Mock()
mock_instance = Mock()
mock_instance.connect.return_value = {"status": "connected"}
mock_instance.query.return_value = [{"id": 1}]
MockDatabaseClient.return_value = mock_instance

with MockSysModules([
    ("myapp.database", "DatabaseClient", MockDatabaseClient),
]):
    from myapp.services import data_service

    result = data_service.get_all_records()
    MockDatabaseClient.assert_called_once()

Configuration with conftest.py

Set project-wide defaults in your conftest.py:

# conftest.py
from deep_mock import DeepMockConfig

def pytest_configure(config):
    DeepMockConfig.configure(
        base_dir="src",  # Base directory to scan for imports
        allowed_dirs=["src/myapp"],  # Only scan these directories
    )

Now all MockSysModules usage will use these defaults:

# test_something.py
from deep_mock import MockSysModules

# Uses conftest.py defaults automatically
with MockSysModules([("myapp.database", "fetch_user", mock)]):
    pass

# Override for specific test if needed
with MockSysModules(
    [("myapp.database", "fetch_user", mock)],
    base_dir="other_dir",
):
    pass

Debugging Mock Calls

Use the debugging utilities to inspect mock calls:

from unittest.mock import Mock
from deep_mock import MockSysModules, print_all_mock_calls, find_calls_in_mock_calls

mock_db = Mock()

with MockSysModules([("myapp.database", "db", mock_db)]):
    from myapp.services import user_service
    user_service.create_user({"name": "Alice"})
    user_service.create_user({"name": "Bob"})

    # Print all calls for debugging
    print_all_mock_calls(mock_db)

    # Find specific calls
    save_calls = find_calls_in_mock_calls(
        mock_db,
        "save",
        call_filter=lambda args, kwargs: args[0]["name"] == "Alice"
    )

API Reference

MockSysModules

Context manager for mocking with automatic module reloading.

class MockSysModules:
    def __init__(
        self,
        override_modules: list[tuple[str, str, Any]] | None = None,
        base_dir: str | None = None,
        allowed_dirs: list[str] | None = None,
    ):
        """
        Args:
            override_modules: List of (module_name, attribute_name, mock) tuples.
                - module_name: Full module path (e.g., "myapp.database")
                - attribute_name: Name of the function/class to mock (e.g., "fetch_user")
                - mock: The mock object to replace it with

            base_dir: Base directory to scan for modules that import the mocked
                attributes. Defaults to DeepMockConfig.base_dir or ".".

            allowed_dirs: List of directories to limit scanning to. If None,
                scans all directories under base_dir. Defaults to
                DeepMockConfig.allowed_dirs.
        """

Behavior:

  1. On enter (__enter__):

    • Patches the specified attributes in the source modules
    • Finds all loaded modules that imported these attributes
    • Patches those modules too
    • Reloads all affected modules so module-level state is recomputed with mocks
  2. On exit (__exit__):

    • Restores all original attributes
    • Reloads all affected modules so module-level state is recomputed with real values
    • Also reloads modules that were imported during the context

mock_sys_modules

Function version of MockSysModules. Returns a cleanup function.

def mock_sys_modules(
    override_modules: list[tuple[str, str, Any]] | None = None,
    base_dir: str = ".",
    allowed_dirs: list[str] | None = None,
) -> Callable[[], None]:
    """
    Apply mocks and return a cleanup function.

    Args:
        override_modules: List of (module_name, attribute_name, mock) tuples.
        base_dir: Base directory to scan for imports.
        allowed_dirs: Directories to limit scanning to.

    Returns:
        A cleanup function that restores original values and reloads modules.

    Example:
        cleanup = mock_sys_modules([("myapp.db", "fetch", mock)])
        try:
            # ... test code ...
        finally:
            cleanup()
    """

import_and_reload_module

Import a module, or reload it if already imported. Essential for handling indirect dependencies.

def import_and_reload_module(module_name: str) -> ModuleType:
    """
    Import or reload a module, returning the module object.

    This is necessary for modules with INDIRECT dependencies on mocked functions.
    These modules import from other modules (not directly from the mocked module),
    so they are not automatically detected and reloaded by MockSysModules.

    Args:
        module_name: Full module path (e.g., "myapp.services.user_service")

    Returns:
        The imported/reloaded module object.

    Example:
        # user_service imports from cache, which imports from database
        # When we mock database.fetch_user, user_service is not auto-reloaded

        with MockSysModules([("myapp.database", "fetch_user", mock)]):
            # Manually reload to recompute module-level state
            user_service = import_and_reload_module("myapp.services.user_service")
            assert user_service.CACHED_VALUE == "mocked value"
    """

When to use:

  • Module has module-level state computed from an indirect dependency
  • Module was imported before entering MockSysModules and has indirect dependencies
  • You need to force a reload at a specific point in your test

DeepMockConfig

Global configuration for deep-mock defaults. Configure once in conftest.py.

class DeepMockConfig:
    base_dir: str = "."
    allowed_dirs: list[str] | None = None

    @classmethod
    def configure(
        cls,
        base_dir: str | None = None,
        allowed_dirs: list[str] | None = None,
    ):
        """
        Set default values for MockSysModules.

        Args:
            base_dir: Default base directory for scanning modules.
            allowed_dirs: Default directories to limit scanning to.

        Example:
            # In conftest.py
            def pytest_configure(config):
                DeepMockConfig.configure(
                    base_dir="src",
                    allowed_dirs=["src/myapp", "src/lib"],
                )
        """

    @classmethod
    def reset(cls):
        """Reset configuration to defaults."""

find_calls_in_mock_calls

Filter mock call history by name and optional predicate.

def find_calls_in_mock_calls(
    mock,
    call_name: str,
    call_filter: Callable[[tuple, dict[str, Any]], bool] | None = None,
) -> list[tuple[str, tuple, dict]]:
    """
    Find specific calls in a mock's call history.

    Args:
        mock: The mock object to inspect.
        call_name: Name of the method call to find (e.g., "save", "().query").
        call_filter: Optional function (args, kwargs) -> bool to filter calls.

    Returns:
        List of (call_name, args, kwargs) tuples matching the criteria.

    Example:
        # Find all 'save' calls where the first arg has status='active'
        calls = find_calls_in_mock_calls(
            mock_db,
            "save",
            call_filter=lambda args, kwargs: args[0]["status"] == "active"
        )
        assert len(calls) == 2
    """

print_all_mock_calls

Debug utility to print all calls made to a mock.

def print_all_mock_calls(mock):
    """
    Print all calls made to a mock object for debugging.

    Prints each call with:
    - Call name (e.g., "", "().method", "().method().chain")
    - Call args (tuple)
    - Call kwargs (dict)

    Example output:
        --------------------------------
             Printing all mock calls
        --------------------------------
        Call name   - type: <class 'str'> ().collection
        Call args   - type: <class 'tuple'> ('users',)
        Call kwargs - type: <class 'dict'> {}
        --------------------------------
    """

fake_useless_decorator

A pass-through decorator for replacing real decorators in tests.

def fake_useless_decorator(func):
    """
    A decorator that does nothing - just returns the function as-is.

    Useful for mocking decorators that have side effects you want to avoid
    in tests (e.g., caching, authentication, rate limiting).

    Example:
        with MockSysModules([
            ("myapp.decorators", "require_auth", fake_useless_decorator),
            ("myapp.decorators", "cache_result", fake_useless_decorator),
        ]):
            # Decorators are now no-ops
            from myapp.api import handler
            handler.protected_endpoint()  # No auth check
    """

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

deep_mock-0.1.0.tar.gz (21.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

deep_mock-0.1.0-py3-none-any.whl (15.3 kB view details)

Uploaded Python 3

File details

Details for the file deep_mock-0.1.0.tar.gz.

File metadata

  • Download URL: deep_mock-0.1.0.tar.gz
  • Upload date:
  • Size: 21.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for deep_mock-0.1.0.tar.gz
Algorithm Hash digest
SHA256 700c27898febe07977b518711609a65ecf88bacd62828ddd4cf1cfdfc2997c77
MD5 70f6541007d3c5bccb71d47e00b32dc8
BLAKE2b-256 be5d2dd363e8166245731c2f1ee72a2ae9192d4d656dbe79837c65d9a96e42e4

See more details on using hashes here.

File details

Details for the file deep_mock-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: deep_mock-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 15.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for deep_mock-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c42a74b7933a85ad6c070d6661c107d4a48db101515d2a8cf473eb680ad2957b
MD5 515c348d37de89b3f6bf013bf18ff62b
BLAKE2b-256 2379ed4ed44a06bb2cc4cba2d7927524a33e3a2560bb9e2c1f0c2100da9072cf

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page