Skip to content

API Reference

mongo_bakery.bakery

Baker

Source code in mongo_bakery/bakery.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
class Baker:
    def __init__(self, mock_class=None):
        self._dependencies_to_patch = mock_class or []
        self._created_instances = []
        self._generation_chain = []

    def mock_dependencies(self, mock_class: list):
        """
        Mocks the specified dependencies for testing purposes.

        Args:
            mock_class (list): A list of classes or modules to be mocked.
        """
        self._dependencies_to_patch = mock_class

    def make(
        self, document_class: type[Document], _quantity: int = 1, **kwargs: dict[Any, Any]
    ) -> Document | list[Document]:
        """
        Creates and saves one or more instances of a MongoEngine document.

        Args:
            document_class (type[Document]): The MongoEngine document class to instantiate.
            _quantity (int, optional): The number of instances to create. Defaults to 1.
            **kwargs: Additional field values to set on the document instances.

        Returns:
            Document or list[Document]: A single document instance if _quantity is 1,
            otherwise a list of document instances.

        Raises:
            ValueError: If the provided document_class is not a subclass of mongoengine.Document
                or mongoengine.EmbeddedDocument.
        """
        if not (issubclass(document_class, Document) or issubclass(document_class, EmbeddedDocument)):
            raise ValueError("The document must be a subclass of mongoengine.Document or mongoengine.EmbeddedDocument")

        if document_class in self._generation_chain:
            chain_repr = " -> ".join(cls.__name__ for cls in [*self._generation_chain, document_class])
            raise ValueError(
                f"Cycle detected while generating mock data for required fields: {chain_repr}. "
                "Pass an explicit value via kwargs to break the cycle."
            )

        with self._tracking(document_class), self._signals_disabled(document_class):
            patch_dependencies = self._build_dependency_patches(document_class)

            instances = []
            with ExitStack() as stack:
                for mock in patch_dependencies.values():
                    stack.enter_context(mock)

                for _ in range(_quantity):
                    instance_data = self._build_instance_data(document_class, kwargs)
                    instance = document_class(**instance_data)
                    if not issubclass(document_class, EmbeddedDocument):
                        instance.save()
                        self._created_instances.append(instance)
                    instances.append(instance)

            return instances if _quantity > 1 else instances[0]

    @contextmanager
    def _tracking(self, document_class: type[Document]) -> Iterator[None]:
        """
        Track `document_class` as in-progress for the duration of the block.

        `make` consults `_generation_chain` before entering this context to detect reference
        cycles, so the class must be popped again even if the block raises.

        Args:
            document_class: The document class to add to `_generation_chain` for the block's duration.
        """
        self._generation_chain.append(document_class)
        try:
            yield
        finally:
            self._generation_chain.pop()

    @contextmanager
    def _signals_disabled(self, document_class: type[Document]) -> Iterator[None]:
        """
        Disconnect `document_class`'s `post_save` signal for the duration of the block.

        Args:
            document_class: The document class whose `post_save` signal should be silenced.
        """
        has_post_save = hasattr(document_class, "post_save")
        if has_post_save:
            signals.post_save.disconnect(document_class.post_save, sender=document_class)
        try:
            yield
        finally:
            if has_post_save:
                signals.post_save.connect(document_class.post_save, sender=document_class)

    def _build_dependency_patches(self, document_class: type[Document]) -> dict[str, Any]:
        """
        Build the `unittest.mock.patch` objects for dependencies actually referenced by the document's module.

        Source-scans the module so only dependencies whose name appears in it are patched, avoiding
        `patch()` calls that would fail against unrelated modules that don't import that dependency.

        Args:
            document_class: The document class whose defining module should be scanned.

        Returns:
            dict[str, Any]: A mapping of dependency name to its not-yet-started `patch` object.
        """
        patch_dependencies = {}
        module_name = document_class.__module__

        if self._dependencies_to_patch and module_name in sys.modules:
            module = sys.modules[module_name]
            try:
                source_lines = inspect.getsource(module).splitlines()
            except (OSError, TypeError):
                source_lines = []
            for dep in self._dependencies_to_patch:
                if any(re.search(rf"\b{re.escape(dep)}\b", line) for line in source_lines):
                    patch_dependencies[dep] = patch(f"{module_name}.{dep}", new=MagicMock())

        return patch_dependencies

    def _build_instance_data(self, document_class: type[Document], kwargs: dict[Any, Any]) -> dict[str, Any]:
        """
        Resolve constructor kwargs for a single instance of `document_class`.

        For each required field not already covered by `kwargs`, uses the field's declared default
        if it has one, otherwise generates mock data. `kwargs` are then overlaid on top, and any
        resulting `Sequence` values are resolved to their next value.

        Args:
            document_class: The document class whose fields should be resolved.
            kwargs: Explicit field values passed to `make`, which take precedence over defaults/mocks.

        Returns:
            dict[str, Any]: Field values ready to pass to `document_class(**instance_data)`.
        """
        instance_data = {}
        for field_name, field in document_class._fields.items():
            if field_name in kwargs or field_name == "id":
                continue
            if not field.required:
                continue

            if field.default is not None:
                default_value = field.default() if callable(field.default) else field.default
                if default_value or not hasattr(field, "field"):
                    instance_data[field_name] = default_value
                    continue

            instance_data[field_name] = self._generate_mock_data(field)

        instance_data.update(kwargs)
        for field_name, value in instance_data.items():
            if isinstance(value, Sequence):
                instance_data[field_name] = value()

        return instance_data

    def seq(
        self,
        value: str | int | float | date | datetime,
        increment_by: int | float | timedelta = 1,
        start: int | float | timedelta | None = None,
    ) -> Sequence:
        """
        Build a sequence that yields an incrementing value each time `make` creates an instance.

        Args:
            value: The base value. Supported types are str, int, float, date and datetime.
            increment_by: The amount added on every call. Defaults to 1. For date/datetime values,
                this must be a timedelta.
            start: The offset applied on the first call. Defaults to `increment_by`.

        Returns:
            Sequence: A callable object that `make` resolves to a new value for each instance.
        """
        return Sequence(value, increment_by=increment_by, start=start)

    def seed(self, value: SeedType) -> None:
        """
        Seed Faker's shared random generator, so `make` produces reproducible mock data.

        `Faker.seed` seeds a random generator shared by every `Faker()` instance by default,
        so this affects mock data generated anywhere in mongo_bakery, not just this module.

        Args:
            value: The seed value, passed through to `Faker.seed`.
        """
        Faker.seed(value)

    def _generate_mock_data(self, field):
        """
        Generate mock data based on the provided field type.

        Args:
            field: The Field type used in the convention. @see the bakery_fields_generators module.

        Returns:
            Any: Mock data appropriate for the given field type.

        """
        if field.choices:
            return self._mock_choice(field)

        field_type = type(field).__name__
        mock_method_name = f"mock_{field_type}"
        mock_method = getattr(bakery_fields_generators, mock_method_name, self._mock_default)

        if "baker" in inspect.signature(mock_method).parameters:
            return mock_method(field, self)
        return mock_method(field)

    def _mock_choice(self, field):
        """
        Pick a random value from a field's `choices` so the result always passes mongoengine's choices validation.

        Args:
            field: The Field instance whose `choices` attribute should be used.

        Returns:
            Any: One of the valid values declared in `field.choices`.
        """
        choice = faker.random_element(field.choices)
        return choice[0] if isinstance(choice, list | tuple) else choice

    def _mock_default(self, field):
        """When there is no match for the field type."""
        raise ValueError(f"No mock defined for field type: {type(field).__name__}")

    def cleanup(self):
        """
        Delete all created instances.

        This method iterates over all instances stored in the `_created_instances`
        list, calls their `delete` method to remove them, and then clears the list.
        """
        for instance in self._created_instances:
            instance.delete()
        self._created_instances.clear()

cleanup()

Delete all created instances.

This method iterates over all instances stored in the _created_instances list, calls their delete method to remove them, and then clears the list.

Source code in mongo_bakery/bakery.py
253
254
255
256
257
258
259
260
261
262
def cleanup(self):
    """
    Delete all created instances.

    This method iterates over all instances stored in the `_created_instances`
    list, calls their `delete` method to remove them, and then clears the list.
    """
    for instance in self._created_instances:
        instance.delete()
    self._created_instances.clear()

make(document_class, _quantity=1, **kwargs)

Creates and saves one or more instances of a MongoEngine document.

Parameters:

Name Type Description Default
document_class type[Document]

The MongoEngine document class to instantiate.

required
_quantity int

The number of instances to create. Defaults to 1.

1
**kwargs dict[Any, Any]

Additional field values to set on the document instances.

{}

Returns:

Type Description
Document | list[Document]

Document or list[Document]: A single document instance if _quantity is 1,

Document | list[Document]

otherwise a list of document instances.

Raises:

Type Description
ValueError

If the provided document_class is not a subclass of mongoengine.Document or mongoengine.EmbeddedDocument.

Source code in mongo_bakery/bakery.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def make(
    self, document_class: type[Document], _quantity: int = 1, **kwargs: dict[Any, Any]
) -> Document | list[Document]:
    """
    Creates and saves one or more instances of a MongoEngine document.

    Args:
        document_class (type[Document]): The MongoEngine document class to instantiate.
        _quantity (int, optional): The number of instances to create. Defaults to 1.
        **kwargs: Additional field values to set on the document instances.

    Returns:
        Document or list[Document]: A single document instance if _quantity is 1,
        otherwise a list of document instances.

    Raises:
        ValueError: If the provided document_class is not a subclass of mongoengine.Document
            or mongoengine.EmbeddedDocument.
    """
    if not (issubclass(document_class, Document) or issubclass(document_class, EmbeddedDocument)):
        raise ValueError("The document must be a subclass of mongoengine.Document or mongoengine.EmbeddedDocument")

    if document_class in self._generation_chain:
        chain_repr = " -> ".join(cls.__name__ for cls in [*self._generation_chain, document_class])
        raise ValueError(
            f"Cycle detected while generating mock data for required fields: {chain_repr}. "
            "Pass an explicit value via kwargs to break the cycle."
        )

    with self._tracking(document_class), self._signals_disabled(document_class):
        patch_dependencies = self._build_dependency_patches(document_class)

        instances = []
        with ExitStack() as stack:
            for mock in patch_dependencies.values():
                stack.enter_context(mock)

            for _ in range(_quantity):
                instance_data = self._build_instance_data(document_class, kwargs)
                instance = document_class(**instance_data)
                if not issubclass(document_class, EmbeddedDocument):
                    instance.save()
                    self._created_instances.append(instance)
                instances.append(instance)

        return instances if _quantity > 1 else instances[0]

mock_dependencies(mock_class)

Mocks the specified dependencies for testing purposes.

Parameters:

Name Type Description Default
mock_class list

A list of classes or modules to be mocked.

required
Source code in mongo_bakery/bakery.py
27
28
29
30
31
32
33
34
def mock_dependencies(self, mock_class: list):
    """
    Mocks the specified dependencies for testing purposes.

    Args:
        mock_class (list): A list of classes or modules to be mocked.
    """
    self._dependencies_to_patch = mock_class

seed(value)

Seed Faker's shared random generator, so make produces reproducible mock data.

Faker.seed seeds a random generator shared by every Faker() instance by default, so this affects mock data generated anywhere in mongo_bakery, not just this module.

Parameters:

Name Type Description Default
value SeedType

The seed value, passed through to Faker.seed.

required
Source code in mongo_bakery/bakery.py
202
203
204
205
206
207
208
209
210
211
212
def seed(self, value: SeedType) -> None:
    """
    Seed Faker's shared random generator, so `make` produces reproducible mock data.

    `Faker.seed` seeds a random generator shared by every `Faker()` instance by default,
    so this affects mock data generated anywhere in mongo_bakery, not just this module.

    Args:
        value: The seed value, passed through to `Faker.seed`.
    """
    Faker.seed(value)

seq(value, increment_by=1, start=None)

Build a sequence that yields an incrementing value each time make creates an instance.

Parameters:

Name Type Description Default
value str | int | float | date | datetime

The base value. Supported types are str, int, float, date and datetime.

required
increment_by int | float | timedelta

The amount added on every call. Defaults to 1. For date/datetime values, this must be a timedelta.

1
start int | float | timedelta | None

The offset applied on the first call. Defaults to increment_by.

None

Returns:

Name Type Description
Sequence Sequence

A callable object that make resolves to a new value for each instance.

Source code in mongo_bakery/bakery.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def seq(
    self,
    value: str | int | float | date | datetime,
    increment_by: int | float | timedelta = 1,
    start: int | float | timedelta | None = None,
) -> Sequence:
    """
    Build a sequence that yields an incrementing value each time `make` creates an instance.

    Args:
        value: The base value. Supported types are str, int, float, date and datetime.
        increment_by: The amount added on every call. Defaults to 1. For date/datetime values,
            this must be a timedelta.
        start: The offset applied on the first call. Defaults to `increment_by`.

    Returns:
        Sequence: A callable object that `make` resolves to a new value for each instance.
    """
    return Sequence(value, increment_by=increment_by, start=start)

mongo_bakery.bakery_fields_generators

mongo_bakery.sequences

Sequence

Produces an incrementing value on each call, for use as a baker.make kwarg.

Source code in mongo_bakery/sequences.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Sequence:
    """Produces an incrementing value on each call, for use as a `baker.make` kwarg."""

    def __init__(self, value, increment_by=1, start=None):
        self.value = value
        self.increment_by = increment_by
        self._step = start if start is not None else increment_by

    def __call__(self):
        if isinstance(self.value, str):
            result = f"{self.value}{self._step}"
        elif isinstance(self.value, (int, float, datetime.date, datetime.datetime)):
            result = self.value + self._step
        else:
            raise ValueError(f"No sequence strategy defined for value type: {type(self.value).__name__}")

        self._step += self.increment_by
        return result

mongo_bakery.pytest_plugin

baker()

Yield the shared mongo_bakery baker and clean up any instances it created after the test.

Registered as a pytest plugin (see the pytest11 entry point in pyproject.toml), so this fixture is available in any project that has mongo_bakery installed, with no extra setup.

Yields:

Name Type Description
Baker Baker

The shared mongo_bakery baker instance.

Source code in mongo_bakery/pytest_plugin.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
@pytest.fixture
def baker() -> Generator[Baker, None, None]:
    """
    Yield the shared `mongo_bakery` baker and clean up any instances it created after the test.

    Registered as a pytest plugin (see the `pytest11` entry point in pyproject.toml), so this
    fixture is available in any project that has `mongo_bakery` installed, with no extra setup.

    Yields:
        Baker: The shared `mongo_bakery` baker instance.
    """
    yield _baker
    _baker.cleanup()