Skip to content

API Reference

mongo_bakery.bakery

Baker

Source code in mongo_bakery/bakery.py
 20
 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
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."
            )

        self._generation_chain.append(document_class)
        try:
            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())

            # Temporarily disable signals
            if hasattr(document_class, "post_save"):
                signals.post_save.disconnect(document_class.post_save, sender=document_class)

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

                for _ in range(_quantity):
                    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()

                    instance = document_class(**instance_data)
                    if not issubclass(document_class, EmbeddedDocument):
                        instance.save()
                        self._created_instances.append(instance)
                    instances.append(instance)

            # Reconnect the signal after creating the instances
            if hasattr(document_class, "post_save"):
                signals.post_save.connect(document_class.post_save, sender=document_class)

            return instances if _quantity > 1 else instances[0]
        finally:
            self._generation_chain.pop()

    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 field_type in {
            "EmbeddedDocumentField",
            "ReferenceField",
            "ListField",
            "EmbeddedDocumentListField",
            "MapField",
            "LazyReferenceField",
            "GenericReferenceField",
        }:
            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
202
203
204
205
206
207
208
209
210
211
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
 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
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."
        )

    self._generation_chain.append(document_class)
    try:
        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())

        # Temporarily disable signals
        if hasattr(document_class, "post_save"):
            signals.post_save.disconnect(document_class.post_save, sender=document_class)

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

            for _ in range(_quantity):
                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()

                instance = document_class(**instance_data)
                if not issubclass(document_class, EmbeddedDocument):
                    instance.save()
                    self._created_instances.append(instance)
                instances.append(instance)

        # Reconnect the signal after creating the instances
        if hasattr(document_class, "post_save"):
            signals.post_save.connect(document_class.post_save, sender=document_class)

        return instances if _quantity > 1 else instances[0]
    finally:
        self._generation_chain.pop()

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
26
27
28
29
30
31
32
33
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
143
144
145
146
147
148
149
150
151
152
153
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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()