model-bakery 1.19.2__py3-none-any.whl → 1.21.0__py3-none-any.whl
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.
Potentially problematic release.
This version of model-bakery might be problematic. Click here for more details.
- model_bakery/__about__.py +1 -1
- model_bakery/baker.py +100 -82
- model_bakery/generators.py +8 -7
- model_bakery/random_gen.py +66 -15
- model_bakery/recipe.py +27 -27
- model_bakery/timezone.py +2 -2
- model_bakery/utils.py +8 -4
- {model_bakery-1.19.2.dist-info → model_bakery-1.21.0.dist-info}/METADATA +39 -30
- model_bakery-1.21.0.dist-info/RECORD +19 -0
- {model_bakery-1.19.2.dist-info → model_bakery-1.21.0.dist-info}/WHEEL +1 -1
- model_bakery-1.19.2.dist-info/RECORD +0 -19
- {model_bakery-1.19.2.dist-info → model_bakery-1.21.0.dist-info}/licenses/LICENSE +0 -0
model_bakery/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "1.21.0"
|
model_bakery/baker.py
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import collections
|
|
2
|
+
from collections.abc import Callable, Iterator
|
|
2
3
|
from os.path import dirname, join
|
|
3
4
|
from typing import (
|
|
4
5
|
Any,
|
|
5
|
-
Callable,
|
|
6
|
-
Dict,
|
|
7
6
|
Generic,
|
|
8
|
-
Iterator,
|
|
9
|
-
List,
|
|
10
|
-
Optional,
|
|
11
|
-
Set,
|
|
12
|
-
Type,
|
|
13
|
-
Union,
|
|
14
7
|
cast,
|
|
15
8
|
overload,
|
|
16
9
|
)
|
|
@@ -27,6 +20,7 @@ from django.db.models import (
|
|
|
27
20
|
Model,
|
|
28
21
|
OneToOneField,
|
|
29
22
|
)
|
|
23
|
+
from django.db.models.fields import NOT_PROVIDED
|
|
30
24
|
from django.db.models.fields.proxy import OrderWrt
|
|
31
25
|
from django.db.models.fields.related import (
|
|
32
26
|
ReverseManyToOneDescriptor as ForeignRelatedObjectsDescriptor,
|
|
@@ -65,20 +59,24 @@ mock_file_txt = join(dirname(__file__), "mock_file.txt")
|
|
|
65
59
|
MAX_MANY_QUANTITY = 5
|
|
66
60
|
|
|
67
61
|
|
|
68
|
-
def _valid_quantity(quantity:
|
|
62
|
+
def _valid_quantity(quantity: str | int | None) -> bool:
|
|
69
63
|
return quantity is not None and (not isinstance(quantity, int) or quantity < 1)
|
|
70
64
|
|
|
71
65
|
|
|
72
|
-
def
|
|
66
|
+
def _is_auto_datetime_field(field: Field) -> bool:
|
|
67
|
+
return getattr(field, "auto_now_add", False) or getattr(field, "auto_now", False)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def seed(seed: int | float | str | bytes | bytearray | None) -> None:
|
|
73
71
|
Baker.seed(seed)
|
|
74
72
|
|
|
75
73
|
|
|
76
74
|
@overload
|
|
77
75
|
def make(
|
|
78
|
-
_model:
|
|
76
|
+
_model: str | type[M],
|
|
79
77
|
_quantity: None = None,
|
|
80
78
|
make_m2m: bool = False,
|
|
81
|
-
_save_kwargs:
|
|
79
|
+
_save_kwargs: dict[str, Any] | None = None,
|
|
82
80
|
_refresh_after_create: bool = False,
|
|
83
81
|
_create_files: bool = False,
|
|
84
82
|
_using: str = "",
|
|
@@ -89,29 +87,29 @@ def make(
|
|
|
89
87
|
|
|
90
88
|
@overload
|
|
91
89
|
def make(
|
|
92
|
-
_model:
|
|
90
|
+
_model: str | type[M],
|
|
93
91
|
_quantity: int,
|
|
94
92
|
make_m2m: bool = False,
|
|
95
|
-
_save_kwargs:
|
|
93
|
+
_save_kwargs: dict[str, Any] | None = None,
|
|
96
94
|
_refresh_after_create: bool = False,
|
|
97
95
|
_create_files: bool = False,
|
|
98
96
|
_using: str = "",
|
|
99
97
|
_bulk_create: bool = False,
|
|
100
|
-
_fill_optional:
|
|
98
|
+
_fill_optional: list[str] | bool = False,
|
|
101
99
|
**attrs: Any,
|
|
102
|
-
) ->
|
|
100
|
+
) -> list[M]: ...
|
|
103
101
|
|
|
104
102
|
|
|
105
103
|
def make(
|
|
106
104
|
_model,
|
|
107
|
-
_quantity:
|
|
105
|
+
_quantity: int | None = None,
|
|
108
106
|
make_m2m: bool = False,
|
|
109
|
-
_save_kwargs:
|
|
107
|
+
_save_kwargs: dict[str, Any] | None = None,
|
|
110
108
|
_refresh_after_create: bool = False,
|
|
111
109
|
_create_files: bool = False,
|
|
112
110
|
_using: str = "",
|
|
113
111
|
_bulk_create: bool = False,
|
|
114
|
-
_fill_optional:
|
|
112
|
+
_fill_optional: list[str] | bool = False,
|
|
115
113
|
**attrs: Any,
|
|
116
114
|
):
|
|
117
115
|
"""Create a persisted instance from a given model its associated models.
|
|
@@ -146,32 +144,32 @@ def make(
|
|
|
146
144
|
|
|
147
145
|
@overload
|
|
148
146
|
def prepare(
|
|
149
|
-
_model:
|
|
147
|
+
_model: str | type[M],
|
|
150
148
|
_quantity: None = None,
|
|
151
149
|
_save_related: bool = False,
|
|
152
150
|
_using: str = "",
|
|
153
|
-
**attrs,
|
|
151
|
+
**attrs: Any,
|
|
154
152
|
) -> M: ...
|
|
155
153
|
|
|
156
154
|
|
|
157
155
|
@overload
|
|
158
156
|
def prepare(
|
|
159
|
-
_model:
|
|
157
|
+
_model: str | type[M],
|
|
160
158
|
_quantity: int,
|
|
161
159
|
_save_related: bool = False,
|
|
162
160
|
_using: str = "",
|
|
163
|
-
_fill_optional:
|
|
164
|
-
**attrs,
|
|
165
|
-
) ->
|
|
161
|
+
_fill_optional: list[str] | bool = False,
|
|
162
|
+
**attrs: Any,
|
|
163
|
+
) -> list[M]: ...
|
|
166
164
|
|
|
167
165
|
|
|
168
166
|
def prepare(
|
|
169
|
-
_model:
|
|
170
|
-
_quantity:
|
|
167
|
+
_model: str | type[M],
|
|
168
|
+
_quantity: int | None = None,
|
|
171
169
|
_save_related: bool = False,
|
|
172
170
|
_using: str = "",
|
|
173
|
-
_fill_optional:
|
|
174
|
-
**attrs,
|
|
171
|
+
_fill_optional: list[str] | bool = False,
|
|
172
|
+
**attrs: Any,
|
|
175
173
|
):
|
|
176
174
|
"""Create but do not persist an instance from a given model.
|
|
177
175
|
|
|
@@ -219,10 +217,10 @@ def prepare_recipe(
|
|
|
219
217
|
class ModelFinder:
|
|
220
218
|
"""Encapsulates all the logic for finding a model to Baker."""
|
|
221
219
|
|
|
222
|
-
_unique_models:
|
|
223
|
-
_ambiguous_models:
|
|
220
|
+
_unique_models: dict[str, type[Model]] | None = None
|
|
221
|
+
_ambiguous_models: list[str] | None = None
|
|
224
222
|
|
|
225
|
-
def get_model(self, name: str) ->
|
|
223
|
+
def get_model(self, name: str) -> type[Model]:
|
|
226
224
|
"""Get a model.
|
|
227
225
|
|
|
228
226
|
Args:
|
|
@@ -246,7 +244,7 @@ class ModelFinder:
|
|
|
246
244
|
|
|
247
245
|
return model
|
|
248
246
|
|
|
249
|
-
def get_model_by_name(self, name: str) ->
|
|
247
|
+
def get_model_by_name(self, name: str) -> type[Model] | None:
|
|
250
248
|
"""Get a model by name.
|
|
251
249
|
|
|
252
250
|
If a model with that name exists in more than one app, raises
|
|
@@ -257,13 +255,13 @@ class ModelFinder:
|
|
|
257
255
|
if self._unique_models is None or self._ambiguous_models is None:
|
|
258
256
|
self._populate()
|
|
259
257
|
|
|
260
|
-
if name in cast(
|
|
258
|
+
if name in cast(list, self._ambiguous_models):
|
|
261
259
|
raise AmbiguousModelName(
|
|
262
260
|
f"{name.title()} is a model in more than one app. "
|
|
263
261
|
'Use the form "app.model".'
|
|
264
262
|
)
|
|
265
263
|
|
|
266
|
-
return cast(
|
|
264
|
+
return cast(dict, self._unique_models).get(name)
|
|
267
265
|
|
|
268
266
|
def _populate(self) -> None:
|
|
269
267
|
"""Cache models for faster self._get_model."""
|
|
@@ -290,7 +288,7 @@ def is_iterator(value: Any) -> bool:
|
|
|
290
288
|
return isinstance(value, collections.abc.Iterator)
|
|
291
289
|
|
|
292
290
|
|
|
293
|
-
def _custom_baker_class() ->
|
|
291
|
+
def _custom_baker_class() -> type | None:
|
|
294
292
|
"""Return the specified custom baker class.
|
|
295
293
|
|
|
296
294
|
Returns:
|
|
@@ -321,54 +319,54 @@ def _custom_baker_class() -> Optional[Type]:
|
|
|
321
319
|
class Baker(Generic[M]):
|
|
322
320
|
SENTINEL = object()
|
|
323
321
|
|
|
324
|
-
attr_mapping:
|
|
325
|
-
type_mapping:
|
|
322
|
+
attr_mapping: dict[str, Any] = {}
|
|
323
|
+
type_mapping: dict = {}
|
|
326
324
|
|
|
327
|
-
_global_seed:
|
|
325
|
+
_global_seed: object | int | float | str | bytes | bytearray | None = SENTINEL
|
|
328
326
|
|
|
329
327
|
# Note: we're using one finder for all Baker instances to avoid
|
|
330
328
|
# rebuilding the model cache for every make_* or prepare_* call.
|
|
331
329
|
finder = ModelFinder()
|
|
332
330
|
|
|
333
331
|
@classmethod
|
|
334
|
-
def seed(cls, seed:
|
|
332
|
+
def seed(cls, seed: int | float | str | bytes | bytearray | None) -> None:
|
|
335
333
|
random_gen.baker_random.seed(seed)
|
|
336
334
|
cls._global_seed = seed
|
|
337
335
|
|
|
338
336
|
@classmethod
|
|
339
337
|
def create(
|
|
340
338
|
cls,
|
|
341
|
-
_model:
|
|
339
|
+
_model: str | type[NewM],
|
|
342
340
|
make_m2m: bool = False,
|
|
343
341
|
create_files: bool = False,
|
|
344
342
|
_using: str = "",
|
|
345
343
|
) -> "Baker[NewM]":
|
|
346
344
|
"""Create the baker class defined by the `BAKER_CUSTOM_CLASS` setting."""
|
|
347
345
|
baker_class = _custom_baker_class() or cls
|
|
348
|
-
return cast(
|
|
346
|
+
return cast(type[Baker[NewM]], baker_class)(
|
|
349
347
|
_model, make_m2m, create_files, _using=_using
|
|
350
348
|
)
|
|
351
349
|
|
|
352
350
|
def __init__(
|
|
353
351
|
self,
|
|
354
|
-
_model:
|
|
352
|
+
_model: str | type[M],
|
|
355
353
|
make_m2m: bool = False,
|
|
356
354
|
create_files: bool = False,
|
|
357
355
|
_using: str = "",
|
|
358
356
|
) -> None:
|
|
359
357
|
self.make_m2m = make_m2m
|
|
360
358
|
self.create_files = create_files
|
|
361
|
-
self.m2m_dict:
|
|
362
|
-
self.iterator_attrs:
|
|
363
|
-
self.model_attrs:
|
|
364
|
-
self.rel_attrs:
|
|
365
|
-
self.rel_fields:
|
|
359
|
+
self.m2m_dict: dict[str, list] = {}
|
|
360
|
+
self.iterator_attrs: dict[str, Iterator] = {}
|
|
361
|
+
self.model_attrs: dict[str, Any] = {}
|
|
362
|
+
self.rel_attrs: dict[str, Any] = {}
|
|
363
|
+
self.rel_fields: list[str] = []
|
|
366
364
|
self._using = _using
|
|
367
365
|
|
|
368
366
|
if isinstance(_model, str):
|
|
369
|
-
self.model = cast(
|
|
367
|
+
self.model = cast(type[M], self.finder.get_model(_model))
|
|
370
368
|
else:
|
|
371
|
-
self.model = cast(
|
|
369
|
+
self.model = cast(type[M], _model)
|
|
372
370
|
|
|
373
371
|
self.init_type_mapping()
|
|
374
372
|
|
|
@@ -382,10 +380,10 @@ class Baker(Generic[M]):
|
|
|
382
380
|
|
|
383
381
|
def make(
|
|
384
382
|
self,
|
|
385
|
-
_save_kwargs:
|
|
383
|
+
_save_kwargs: dict[str, Any] | None = None,
|
|
386
384
|
_refresh_after_create: bool = False,
|
|
387
385
|
_from_manager=None,
|
|
388
|
-
_fill_optional:
|
|
386
|
+
_fill_optional: list[str] | bool = False,
|
|
389
387
|
**attrs: Any,
|
|
390
388
|
):
|
|
391
389
|
"""Create and persist an instance of the model associated with Baker instance."""
|
|
@@ -403,7 +401,7 @@ class Baker(Generic[M]):
|
|
|
403
401
|
def prepare(
|
|
404
402
|
self,
|
|
405
403
|
_save_related=False,
|
|
406
|
-
_fill_optional:
|
|
404
|
+
_fill_optional: list[str] | bool = False,
|
|
407
405
|
**attrs: Any,
|
|
408
406
|
) -> M:
|
|
409
407
|
"""Create, but do not persist, an instance of the associated model."""
|
|
@@ -415,7 +413,7 @@ class Baker(Generic[M]):
|
|
|
415
413
|
params.update(attrs)
|
|
416
414
|
return self._make(**params)
|
|
417
415
|
|
|
418
|
-
def get_fields(self) ->
|
|
416
|
+
def get_fields(self) -> set[Any]:
|
|
419
417
|
return set(self.model._meta.get_fields()) - set(
|
|
420
418
|
self.model._meta.related_objects
|
|
421
419
|
)
|
|
@@ -489,7 +487,7 @@ class Baker(Generic[M]):
|
|
|
489
487
|
|
|
490
488
|
return instance
|
|
491
489
|
|
|
492
|
-
def m2m_value(self, field: ManyToManyField) ->
|
|
490
|
+
def m2m_value(self, field: ManyToManyField) -> list[Any]:
|
|
493
491
|
if field.name in self.rel_fields:
|
|
494
492
|
return self.generate_value(field)
|
|
495
493
|
if not self.make_m2m or field.null and not field.fill_optional:
|
|
@@ -497,7 +495,7 @@ class Baker(Generic[M]):
|
|
|
497
495
|
return self.generate_value(field)
|
|
498
496
|
|
|
499
497
|
def instance(
|
|
500
|
-
self, attrs:
|
|
498
|
+
self, attrs: dict[str, Any], _commit, _save_kwargs, _from_manager
|
|
501
499
|
) -> M:
|
|
502
500
|
one_to_many_keys = {}
|
|
503
501
|
auto_now_keys = {}
|
|
@@ -512,10 +510,7 @@ class Baker(Generic[M]):
|
|
|
512
510
|
if isinstance(field, ForeignRelatedObjectsDescriptor):
|
|
513
511
|
one_to_many_keys[k] = attrs.pop(k)
|
|
514
512
|
|
|
515
|
-
if hasattr(field, "field") and (
|
|
516
|
-
getattr(field.field, "auto_now_add", False)
|
|
517
|
-
or getattr(field.field, "auto_now", False)
|
|
518
|
-
):
|
|
513
|
+
if hasattr(field, "field") and _is_auto_datetime_field(field.field):
|
|
519
514
|
auto_now_keys[k] = attrs[k]
|
|
520
515
|
|
|
521
516
|
if BAKER_CONTENTTYPES and isinstance(field, GenericForeignKey):
|
|
@@ -523,6 +518,7 @@ class Baker(Generic[M]):
|
|
|
523
518
|
"value": attrs.pop(k),
|
|
524
519
|
"content_type_field": field.ct_field,
|
|
525
520
|
"object_id_field": field.fk_field,
|
|
521
|
+
"for_concrete_model": field.for_concrete_model,
|
|
526
522
|
}
|
|
527
523
|
|
|
528
524
|
instance = self.model(**attrs)
|
|
@@ -546,7 +542,7 @@ class Baker(Generic[M]):
|
|
|
546
542
|
return instance
|
|
547
543
|
|
|
548
544
|
def create_by_related_name(
|
|
549
|
-
self, instance: Model, related:
|
|
545
|
+
self, instance: Model, related: ManyToOneRel | OneToOneRel
|
|
550
546
|
) -> None:
|
|
551
547
|
rel_name = related.get_accessor_name()
|
|
552
548
|
if not rel_name or rel_name not in self.rel_fields:
|
|
@@ -557,7 +553,7 @@ class Baker(Generic[M]):
|
|
|
557
553
|
|
|
558
554
|
make(related.field.model, **kwargs)
|
|
559
555
|
|
|
560
|
-
def _clean_attrs(self, attrs:
|
|
556
|
+
def _clean_attrs(self, attrs: dict[str, Any]) -> None:
|
|
561
557
|
def is_rel_field(x: str):
|
|
562
558
|
return "__" in x
|
|
563
559
|
|
|
@@ -638,13 +634,18 @@ class Baker(Generic[M]):
|
|
|
638
634
|
|
|
639
635
|
return False
|
|
640
636
|
|
|
641
|
-
def _handle_auto_now(self, instance: Model, attrs:
|
|
637
|
+
def _handle_auto_now(self, instance: Model, attrs: dict[str, Any]):
|
|
642
638
|
if not attrs:
|
|
643
639
|
return
|
|
644
640
|
|
|
641
|
+
# use .update() to force update auto_now fields
|
|
645
642
|
instance.__class__.objects.filter(pk=instance.pk).update(**attrs)
|
|
646
643
|
|
|
647
|
-
|
|
644
|
+
# to make the resulting instance has the specified values
|
|
645
|
+
for k, v in attrs.items():
|
|
646
|
+
setattr(instance, k, v)
|
|
647
|
+
|
|
648
|
+
def _handle_one_to_many(self, instance: Model, attrs: dict[str, Any]):
|
|
648
649
|
for key, values in attrs.items():
|
|
649
650
|
manager = getattr(instance, key)
|
|
650
651
|
|
|
@@ -692,26 +693,38 @@ class Baker(Generic[M]):
|
|
|
692
693
|
}
|
|
693
694
|
make(through_model, _using=self._using, **base_kwargs)
|
|
694
695
|
|
|
695
|
-
def _handle_generic_foreign_keys(self, instance: Model, attrs:
|
|
696
|
+
def _handle_generic_foreign_keys(self, instance: Model, attrs: dict[str, Any]):
|
|
696
697
|
"""Set content type and object id for GenericForeignKey fields."""
|
|
697
698
|
for field_name, data in attrs.items():
|
|
698
|
-
|
|
699
|
-
|
|
699
|
+
ct_field_name = data["content_type_field"]
|
|
700
|
+
oid_field_name = data["object_id_field"]
|
|
700
701
|
value = data["value"]
|
|
702
|
+
if callable(value):
|
|
703
|
+
value = value()
|
|
701
704
|
if is_iterator(value):
|
|
702
705
|
value = next(value)
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
instance
|
|
707
|
-
content_type_field
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
706
|
+
if value is None:
|
|
707
|
+
# when GFK is None, we should try to set the content type and object id to None
|
|
708
|
+
content_type_field = instance._meta.get_field(ct_field_name)
|
|
709
|
+
object_id_field = instance._meta.get_field(oid_field_name)
|
|
710
|
+
if content_type_field.null:
|
|
711
|
+
setattr(instance, ct_field_name, None)
|
|
712
|
+
if object_id_field.null:
|
|
713
|
+
setattr(instance, oid_field_name, None)
|
|
714
|
+
else:
|
|
715
|
+
setattr(instance, field_name, value)
|
|
716
|
+
setattr(
|
|
717
|
+
instance,
|
|
718
|
+
ct_field_name,
|
|
719
|
+
contenttypes_models.ContentType.objects.get_for_model(
|
|
720
|
+
value, for_concrete_model=data["for_concrete_model"]
|
|
721
|
+
),
|
|
722
|
+
)
|
|
723
|
+
setattr(instance, oid_field_name, value.pk)
|
|
711
724
|
|
|
712
725
|
def _remote_field(
|
|
713
|
-
self, field:
|
|
714
|
-
) ->
|
|
726
|
+
self, field: ForeignKey | OneToOneField
|
|
727
|
+
) -> OneToOneRel | ManyToOneRel:
|
|
715
728
|
return field.remote_field
|
|
716
729
|
|
|
717
730
|
def generate_value(self, field: Field, commit: bool = True) -> Any: # noqa: C901
|
|
@@ -719,6 +732,7 @@ class Baker(Generic[M]):
|
|
|
719
732
|
|
|
720
733
|
Generator Resolution Precedence Order:
|
|
721
734
|
-- `field.default` - model field default value, unless explicitly overwritten during baking
|
|
735
|
+
-- `field.db_default` - model field db default value, unless explicitly overwritten
|
|
722
736
|
-- `attr_mapping` - mapping per attribute name
|
|
723
737
|
-- `choices` -- mapping from available field choices
|
|
724
738
|
-- `type_mapping` - mapping from user defined type associated generators
|
|
@@ -742,10 +756,14 @@ class Baker(Generic[M]):
|
|
|
742
756
|
if callable(field.default):
|
|
743
757
|
return field.default()
|
|
744
758
|
return field.default
|
|
759
|
+
elif getattr(field, "db_default", NOT_PROVIDED) != NOT_PROVIDED:
|
|
760
|
+
return field.db_default
|
|
745
761
|
elif field.name in self.attr_mapping:
|
|
746
762
|
generator = self.attr_mapping[field.name]
|
|
747
763
|
elif field.choices:
|
|
748
|
-
generator = random_gen.gen_from_choices(
|
|
764
|
+
generator = random_gen.gen_from_choices(
|
|
765
|
+
field.choices, nullable=field.null, blankable=field.blank
|
|
766
|
+
)
|
|
749
767
|
elif is_content_type_fk:
|
|
750
768
|
generator = self.type_mapping[contenttypes_models.ContentType]
|
|
751
769
|
elif generators.get(field.__class__):
|
|
@@ -780,14 +798,14 @@ class Baker(Generic[M]):
|
|
|
780
798
|
|
|
781
799
|
def get_required_values(
|
|
782
800
|
generator: Callable, field: Field
|
|
783
|
-
) ->
|
|
801
|
+
) -> dict[str, bool | int | str | list[Callable]]:
|
|
784
802
|
"""Get required values for a generator from the field.
|
|
785
803
|
|
|
786
804
|
If required value is a function, calls it with field as argument. If
|
|
787
805
|
required value is a string, simply fetch the value from the field
|
|
788
806
|
and return.
|
|
789
807
|
"""
|
|
790
|
-
required_values = {} # type:
|
|
808
|
+
required_values = {} # type: dict[str, Any]
|
|
791
809
|
if hasattr(generator, "required"):
|
|
792
810
|
for item in generator.required: # type: ignore[attr-defined]
|
|
793
811
|
if callable(item): # baker can deal with the nasty hacking too!
|
|
@@ -805,7 +823,7 @@ def get_required_values(
|
|
|
805
823
|
return required_values
|
|
806
824
|
|
|
807
825
|
|
|
808
|
-
def filter_rel_attrs(field_name: str, **rel_attrs) ->
|
|
826
|
+
def filter_rel_attrs(field_name: str, **rel_attrs) -> dict[str, Any]:
|
|
809
827
|
clean_dict = {}
|
|
810
828
|
|
|
811
829
|
for k, v in rel_attrs.items():
|
|
@@ -841,7 +859,7 @@ def _save_related_objs(model, objects, _using=None) -> None:
|
|
|
841
859
|
setattr(objects[i], fk.name, fk_obj)
|
|
842
860
|
|
|
843
861
|
|
|
844
|
-
def bulk_create(baker: Baker[M], quantity: int, **kwargs) ->
|
|
862
|
+
def bulk_create(baker: Baker[M], quantity: int, **kwargs) -> list[M]:
|
|
845
863
|
"""
|
|
846
864
|
Bulk create entries and all related FKs as well.
|
|
847
865
|
|
model_bakery/generators.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from decimal import Decimal
|
|
2
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
from django.db.backends.base.operations import BaseDatabaseOperations
|
|
5
6
|
from django.db.models import (
|
|
@@ -108,7 +109,7 @@ default_mapping = {
|
|
|
108
109
|
DecimalField: random_gen.gen_decimal,
|
|
109
110
|
BinaryField: random_gen.gen_byte_string,
|
|
110
111
|
CharField: random_gen.gen_string,
|
|
111
|
-
TextField: random_gen.
|
|
112
|
+
TextField: random_gen.gen_string,
|
|
112
113
|
SlugField: random_gen.gen_slug,
|
|
113
114
|
UUIDField: random_gen.gen_uuid,
|
|
114
115
|
DateField: random_gen.gen_date,
|
|
@@ -122,7 +123,7 @@ default_mapping = {
|
|
|
122
123
|
ImageField: random_gen.gen_image_field,
|
|
123
124
|
DurationField: random_gen.gen_interval,
|
|
124
125
|
JSONField: random_gen.gen_json,
|
|
125
|
-
} # type:
|
|
126
|
+
} # type: dict[type, Callable]
|
|
126
127
|
|
|
127
128
|
if ArrayField:
|
|
128
129
|
default_mapping[ArrayField] = random_gen.gen_array
|
|
@@ -133,7 +134,7 @@ if CICharField:
|
|
|
133
134
|
if CIEmailField:
|
|
134
135
|
default_mapping[CIEmailField] = random_gen.gen_email
|
|
135
136
|
if CITextField:
|
|
136
|
-
default_mapping[CITextField] = random_gen.
|
|
137
|
+
default_mapping[CITextField] = random_gen.gen_string
|
|
137
138
|
if DecimalRangeField:
|
|
138
139
|
default_mapping[DecimalRangeField] = random_gen.gen_pg_numbers_range(Decimal)
|
|
139
140
|
if IntegerRangeField:
|
|
@@ -149,7 +150,7 @@ if DateTimeRangeField:
|
|
|
149
150
|
# Add GIS fields
|
|
150
151
|
|
|
151
152
|
|
|
152
|
-
def get_type_mapping() ->
|
|
153
|
+
def get_type_mapping() -> dict[type, Callable]:
|
|
153
154
|
from .content_types import default_contenttypes_mapping
|
|
154
155
|
from .gis import default_gis_mapping
|
|
155
156
|
|
|
@@ -162,9 +163,9 @@ def get_type_mapping() -> Dict[Type, Callable]:
|
|
|
162
163
|
user_mapping = {}
|
|
163
164
|
|
|
164
165
|
|
|
165
|
-
def add(field: str, func:
|
|
166
|
+
def add(field: str, func: Callable | str | None) -> None:
|
|
166
167
|
user_mapping[import_from_str(field)] = import_from_str(func)
|
|
167
168
|
|
|
168
169
|
|
|
169
|
-
def get(field: Any) ->
|
|
170
|
+
def get(field: Any) -> Callable | None:
|
|
170
171
|
return user_mapping.get(field)
|
model_bakery/random_gen.py
CHANGED
|
@@ -11,11 +11,12 @@ argument.
|
|
|
11
11
|
|
|
12
12
|
import string
|
|
13
13
|
import warnings
|
|
14
|
+
from collections.abc import Callable
|
|
14
15
|
from datetime import date, datetime, time, timedelta
|
|
15
16
|
from decimal import Decimal
|
|
16
17
|
from os.path import abspath, dirname, join
|
|
17
18
|
from random import Random
|
|
18
|
-
from typing import Any
|
|
19
|
+
from typing import Any
|
|
19
20
|
from uuid import UUID
|
|
20
21
|
|
|
21
22
|
from django.core.files.base import ContentFile
|
|
@@ -48,7 +49,7 @@ def gen_image_field() -> ContentFile:
|
|
|
48
49
|
return get_content_file(f.read(), name=name)
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
def gen_from_list(a_list:
|
|
52
|
+
def gen_from_list(a_list: list[str] | range) -> Callable:
|
|
52
53
|
"""Make sure all values of the field are generated from a list.
|
|
53
54
|
|
|
54
55
|
Examples:
|
|
@@ -65,14 +66,22 @@ def gen_from_list(a_list: Union[List[str], range]) -> Callable:
|
|
|
65
66
|
# -- DEFAULT GENERATORS --
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def gen_from_choices(
|
|
69
|
+
def gen_from_choices(
|
|
70
|
+
choices: list, nullable: bool = True, blankable: bool = True
|
|
71
|
+
) -> Callable:
|
|
69
72
|
choice_list = []
|
|
70
73
|
for value, label in choices:
|
|
74
|
+
if not nullable and value is None:
|
|
75
|
+
continue
|
|
76
|
+
if not blankable and value == "":
|
|
77
|
+
continue
|
|
78
|
+
|
|
71
79
|
if isinstance(label, (list, tuple)):
|
|
72
80
|
for val, _lbl in label:
|
|
73
81
|
choice_list.append(val)
|
|
74
82
|
else:
|
|
75
83
|
choice_list.append(value)
|
|
84
|
+
|
|
76
85
|
return gen_from_list(choice_list)
|
|
77
86
|
|
|
78
87
|
|
|
@@ -80,8 +89,13 @@ def gen_integer(min_int: int = -MAX_INT, max_int: int = MAX_INT) -> int:
|
|
|
80
89
|
return baker_random.randint(min_int, max_int)
|
|
81
90
|
|
|
82
91
|
|
|
83
|
-
def gen_float() -> float:
|
|
84
|
-
|
|
92
|
+
def gen_float(min_float: float = -1000000.0, max_float: float = 1000000.0) -> float:
|
|
93
|
+
"""
|
|
94
|
+
Generate a random float uniformly distributed between `min_float` and `max_float`.
|
|
95
|
+
|
|
96
|
+
Defaults to ±1,000,000.0 which is suitable for most test data scenarios.
|
|
97
|
+
"""
|
|
98
|
+
return baker_random.uniform(min_float, max_float)
|
|
85
99
|
|
|
86
100
|
|
|
87
101
|
def gen_decimal(max_digits: int, decimal_places: int) -> Decimal:
|
|
@@ -115,7 +129,7 @@ def gen_string(max_length: int) -> str:
|
|
|
115
129
|
return "".join(baker_random.choice(string.ascii_letters) for _ in range(max_length))
|
|
116
130
|
|
|
117
131
|
|
|
118
|
-
def _gen_string_get_max_length(field: Field) ->
|
|
132
|
+
def _gen_string_get_max_length(field: Field) -> tuple[str, int]:
|
|
119
133
|
max_length = getattr(field, "max_length", None)
|
|
120
134
|
if max_length is None:
|
|
121
135
|
max_length = MAX_LENGTH
|
|
@@ -134,6 +148,15 @@ gen_slug.required = ["max_length"] # type: ignore[attr-defined]
|
|
|
134
148
|
|
|
135
149
|
|
|
136
150
|
def gen_text() -> str:
|
|
151
|
+
warnings.warn(
|
|
152
|
+
"\n"
|
|
153
|
+
"Accessing `model_bakery.random_gen.gen_text` is deprecated "
|
|
154
|
+
"and will be removed in a future major release. Please use "
|
|
155
|
+
"`model_bakery.random_gen.gen_string` instead."
|
|
156
|
+
"\n",
|
|
157
|
+
DeprecationWarning,
|
|
158
|
+
stacklevel=2,
|
|
159
|
+
)
|
|
137
160
|
return gen_string(MAX_LENGTH)
|
|
138
161
|
|
|
139
162
|
|
|
@@ -166,7 +189,7 @@ def gen_ipv46() -> str:
|
|
|
166
189
|
return ip_gen()
|
|
167
190
|
|
|
168
191
|
|
|
169
|
-
def gen_ip(protocol: str, default_validators:
|
|
192
|
+
def gen_ip(protocol: str, default_validators: list[Callable]) -> str:
|
|
170
193
|
from django.core.exceptions import ValidationError
|
|
171
194
|
|
|
172
195
|
protocol = (protocol or "").lower()
|
|
@@ -203,9 +226,18 @@ def gen_byte_string(max_length: int = 16) -> bytes:
|
|
|
203
226
|
return bytes(generator)
|
|
204
227
|
|
|
205
228
|
|
|
206
|
-
def gen_interval(
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
def gen_interval(
|
|
230
|
+
interval_key: str = "milliseconds",
|
|
231
|
+
min_interval: int = -365 * 24 * 60 * 60 * 1000,
|
|
232
|
+
max_interval: int = 365 * 24 * 60 * 60 * 1000,
|
|
233
|
+
) -> timedelta:
|
|
234
|
+
"""
|
|
235
|
+
Generate a random timedelta for `DurationField` or date range calculations.
|
|
236
|
+
|
|
237
|
+
Defaults to ±1 year in milliseconds, suitable for most test scenarios.
|
|
238
|
+
The `interval_key` determines which timedelta argument is set (e.g., 'seconds', 'days').
|
|
239
|
+
"""
|
|
240
|
+
kwargs = {interval_key: baker_random.randint(min_interval, max_interval)}
|
|
209
241
|
return timedelta(**kwargs)
|
|
210
242
|
|
|
211
243
|
|
|
@@ -244,7 +276,7 @@ def gen_hstore():
|
|
|
244
276
|
return {}
|
|
245
277
|
|
|
246
278
|
|
|
247
|
-
def _fk_model(field: Field) ->
|
|
279
|
+
def _fk_model(field: Field) -> tuple[str, Model | None]:
|
|
248
280
|
try:
|
|
249
281
|
return ("model", field.related_model)
|
|
250
282
|
except AttributeError:
|
|
@@ -253,7 +285,7 @@ def _fk_model(field: Field) -> Tuple[str, Optional[Model]]:
|
|
|
253
285
|
|
|
254
286
|
def _prepare_related(
|
|
255
287
|
model: str, _create_files=False, **attrs: Any
|
|
256
|
-
) ->
|
|
288
|
+
) -> Model | list[Model]:
|
|
257
289
|
from .baker import prepare
|
|
258
290
|
|
|
259
291
|
return prepare(model, **attrs)
|
|
@@ -333,37 +365,56 @@ def gen_geometry_collection() -> str:
|
|
|
333
365
|
|
|
334
366
|
|
|
335
367
|
def gen_pg_numbers_range(number_cast: Callable[[int], Any]) -> Callable:
|
|
368
|
+
"""
|
|
369
|
+
Factory that returns a generator for PostgreSQL numeric range fields.
|
|
370
|
+
|
|
371
|
+
The returned generator creates ranges like [-N, N] where N is a random value
|
|
372
|
+
between 1 and 100,000, cast to the appropriate numeric type (int, float, etc).
|
|
373
|
+
"""
|
|
374
|
+
|
|
336
375
|
def gen_range():
|
|
337
376
|
try:
|
|
338
377
|
from psycopg.types.range import Range
|
|
339
378
|
except ImportError:
|
|
340
379
|
from psycopg2._range import NumericRange as Range
|
|
341
380
|
|
|
342
|
-
base_num =
|
|
381
|
+
base_num = baker_random.randint(1, 100000)
|
|
343
382
|
return Range(number_cast(-1 * base_num), number_cast(base_num))
|
|
344
383
|
|
|
345
384
|
return gen_range
|
|
346
385
|
|
|
347
386
|
|
|
348
387
|
def gen_date_range():
|
|
388
|
+
"""
|
|
389
|
+
Generate random `DateRange` for PostgreSQL `DateRangeField`.
|
|
390
|
+
|
|
391
|
+
The range spans a random date with a minimum interval of 1 day
|
|
392
|
+
to avoid empty ranges in tests.
|
|
393
|
+
"""
|
|
349
394
|
try:
|
|
350
395
|
from psycopg.types.range import DateRange
|
|
351
396
|
except ImportError:
|
|
352
397
|
from psycopg2.extras import DateRange
|
|
353
398
|
|
|
354
399
|
base_date = gen_date()
|
|
355
|
-
interval = gen_interval(
|
|
400
|
+
interval = gen_interval(min_interval=24 * 60 * 60 * 1000)
|
|
356
401
|
args = sorted([base_date - interval, base_date + interval])
|
|
357
402
|
return DateRange(*args)
|
|
358
403
|
|
|
359
404
|
|
|
360
405
|
def gen_datetime_range():
|
|
406
|
+
"""
|
|
407
|
+
Generate random `TimestamptzRange` for PostgreSQL DateTimeRangeField.
|
|
408
|
+
|
|
409
|
+
The range spans a random datetime with a minimum interval of 1 day
|
|
410
|
+
to avoid empty ranges in tests.
|
|
411
|
+
"""
|
|
361
412
|
try:
|
|
362
413
|
from psycopg.types.range import TimestamptzRange
|
|
363
414
|
except ImportError:
|
|
364
415
|
from psycopg2.extras import DateTimeTZRange as TimestamptzRange
|
|
365
416
|
|
|
366
417
|
base_datetime = gen_datetime()
|
|
367
|
-
interval = gen_interval()
|
|
418
|
+
interval = gen_interval(min_interval=24 * 60 * 60 * 1000)
|
|
368
419
|
args = sorted([base_datetime - interval, base_datetime + interval])
|
|
369
420
|
return TimestamptzRange(*args)
|
model_bakery/recipe.py
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import collections
|
|
2
|
+
import copy
|
|
2
3
|
import itertools
|
|
3
4
|
from typing import (
|
|
4
5
|
Any,
|
|
5
|
-
Dict,
|
|
6
6
|
Generic,
|
|
7
|
-
List,
|
|
8
|
-
Optional,
|
|
9
|
-
Type,
|
|
10
7
|
TypeVar,
|
|
11
|
-
Union,
|
|
12
8
|
cast,
|
|
13
9
|
overload,
|
|
14
10
|
)
|
|
@@ -29,15 +25,15 @@ finder = baker.ModelFinder()
|
|
|
29
25
|
class Recipe(Generic[M]):
|
|
30
26
|
_T = TypeVar("_T", bound="Recipe[M]")
|
|
31
27
|
|
|
32
|
-
def __init__(self, _model:
|
|
28
|
+
def __init__(self, _model: str | type[M], **attrs: Any) -> None:
|
|
33
29
|
self.attr_mapping = attrs
|
|
34
30
|
self._model = _model
|
|
35
31
|
# _iterator_backups will hold values of the form (backup_iterator, usable_iterator).
|
|
36
|
-
self._iterator_backups = {} # type:
|
|
32
|
+
self._iterator_backups = {} # type: dict[str, Any]
|
|
37
33
|
|
|
38
34
|
def _mapping( # noqa: C901
|
|
39
|
-
self, _using: str, new_attrs:
|
|
40
|
-
) ->
|
|
35
|
+
self, _using: str, new_attrs: dict[str, Any]
|
|
36
|
+
) -> dict[str, Any]:
|
|
41
37
|
_save_related = new_attrs.get("_save_related", True)
|
|
42
38
|
_quantity = new_attrs.get("_quantity", 1)
|
|
43
39
|
rel_fields_attrs = {k: v for k, v in new_attrs.items() if "__" in k}
|
|
@@ -79,6 +75,8 @@ class Recipe(Generic[M]):
|
|
|
79
75
|
mapping[k] = v.recipe.prepare(_using=_using, **recipe_attrs)
|
|
80
76
|
elif isinstance(v, related):
|
|
81
77
|
mapping[k] = v.make
|
|
78
|
+
elif isinstance(v, collections.abc.Container):
|
|
79
|
+
mapping[k] = copy.deepcopy(v)
|
|
82
80
|
|
|
83
81
|
mapping.update(new_attrs)
|
|
84
82
|
mapping.update(rel_fields_attrs)
|
|
@@ -93,7 +91,7 @@ class Recipe(Generic[M]):
|
|
|
93
91
|
_create_files: bool = False,
|
|
94
92
|
_using: str = "",
|
|
95
93
|
_bulk_create: bool = False,
|
|
96
|
-
_save_kwargs:
|
|
94
|
+
_save_kwargs: dict[str, Any] | None = None,
|
|
97
95
|
**attrs: Any,
|
|
98
96
|
) -> M: ...
|
|
99
97
|
|
|
@@ -106,21 +104,21 @@ class Recipe(Generic[M]):
|
|
|
106
104
|
_create_files: bool = False,
|
|
107
105
|
_using: str = "",
|
|
108
106
|
_bulk_create: bool = False,
|
|
109
|
-
_save_kwargs:
|
|
107
|
+
_save_kwargs: dict[str, Any] | None = None,
|
|
110
108
|
**attrs: Any,
|
|
111
|
-
) ->
|
|
109
|
+
) -> list[M]: ...
|
|
112
110
|
|
|
113
111
|
def make(
|
|
114
112
|
self,
|
|
115
|
-
_quantity:
|
|
116
|
-
make_m2m:
|
|
117
|
-
_refresh_after_create:
|
|
118
|
-
_create_files:
|
|
113
|
+
_quantity: int | None = None,
|
|
114
|
+
make_m2m: bool | None = None,
|
|
115
|
+
_refresh_after_create: bool | None = None,
|
|
116
|
+
_create_files: bool | None = None,
|
|
119
117
|
_using: str = "",
|
|
120
|
-
_bulk_create:
|
|
121
|
-
_save_kwargs:
|
|
118
|
+
_bulk_create: bool | None = None,
|
|
119
|
+
_save_kwargs: dict[str, Any] | None = None,
|
|
122
120
|
**attrs: Any,
|
|
123
|
-
) ->
|
|
121
|
+
) -> M | list[M]:
|
|
124
122
|
defaults = {}
|
|
125
123
|
if _quantity is not None:
|
|
126
124
|
defaults["_quantity"] = _quantity
|
|
@@ -154,19 +152,21 @@ class Recipe(Generic[M]):
|
|
|
154
152
|
_save_related: bool = False,
|
|
155
153
|
_using: str = "",
|
|
156
154
|
**attrs: Any,
|
|
157
|
-
) ->
|
|
155
|
+
) -> list[M]: ...
|
|
158
156
|
|
|
159
157
|
def prepare(
|
|
160
158
|
self,
|
|
161
|
-
_quantity:
|
|
159
|
+
_quantity: int | None = None,
|
|
162
160
|
_save_related: bool = False,
|
|
163
161
|
_using: str = "",
|
|
164
162
|
**attrs: Any,
|
|
165
|
-
) ->
|
|
163
|
+
) -> M | list[M]:
|
|
166
164
|
defaults = {
|
|
167
|
-
"_quantity": _quantity,
|
|
168
165
|
"_save_related": _save_related,
|
|
169
166
|
}
|
|
167
|
+
if _quantity is not None:
|
|
168
|
+
defaults["_quantity"] = _quantity # type: ignore[assignment]
|
|
169
|
+
|
|
170
170
|
defaults.update(attrs)
|
|
171
171
|
return baker.prepare(
|
|
172
172
|
self._model, _using=_using, **self._mapping(_using, defaults)
|
|
@@ -207,7 +207,7 @@ class RecipeForeignKey(Generic[M]):
|
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
def foreign_key(
|
|
210
|
-
recipe:
|
|
210
|
+
recipe: Recipe[M] | str, one_to_one: bool = False
|
|
211
211
|
) -> RecipeForeignKey[M]:
|
|
212
212
|
"""Return a `RecipeForeignKey`.
|
|
213
213
|
|
|
@@ -230,8 +230,8 @@ def foreign_key(
|
|
|
230
230
|
|
|
231
231
|
|
|
232
232
|
class related(Generic[M]): # FIXME
|
|
233
|
-
def __init__(self, *args:
|
|
234
|
-
self.related = [] # type:
|
|
233
|
+
def __init__(self, *args: str | Recipe[M]) -> None:
|
|
234
|
+
self.related = [] # type: list[Recipe[M]]
|
|
235
235
|
for recipe in args:
|
|
236
236
|
if isinstance(recipe, Recipe):
|
|
237
237
|
self.related.append(recipe)
|
|
@@ -244,6 +244,6 @@ class related(Generic[M]): # FIXME
|
|
|
244
244
|
else:
|
|
245
245
|
raise TypeError("Not a recipe")
|
|
246
246
|
|
|
247
|
-
def make(self) ->
|
|
247
|
+
def make(self) -> list[M | list[M]]:
|
|
248
248
|
"""Persist objects to m2m relation."""
|
|
249
249
|
return [m.make() for m in self.related]
|
model_bakery/timezone.py
CHANGED
|
@@ -8,6 +8,6 @@ from django.conf import settings
|
|
|
8
8
|
def tz_aware(value: datetime) -> datetime:
|
|
9
9
|
"""Return an UTC-aware datetime in case of USE_TZ=True."""
|
|
10
10
|
if settings.USE_TZ:
|
|
11
|
-
|
|
11
|
+
return value.replace(tzinfo=timezone.utc)
|
|
12
12
|
|
|
13
|
-
return value
|
|
13
|
+
return value.replace(tzinfo=None)
|
model_bakery/utils.py
CHANGED
|
@@ -3,8 +3,9 @@ import importlib
|
|
|
3
3
|
import inspect
|
|
4
4
|
import itertools
|
|
5
5
|
import warnings
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from types import ModuleType
|
|
7
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
10
|
from django.apps import apps
|
|
10
11
|
|
|
@@ -13,7 +14,7 @@ from .timezone import tz_aware
|
|
|
13
14
|
__all__ = ["import_from_str", "get_calling_module", "seq"]
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def import_from_str(import_string:
|
|
17
|
+
def import_from_str(import_string: Callable | str | None) -> Any:
|
|
17
18
|
"""Import an object defined as import if it is an string.
|
|
18
19
|
|
|
19
20
|
If `import_string` follows the format `path.to.module.object_name`,
|
|
@@ -33,7 +34,7 @@ def import_from_str(import_string: Optional[Union[Callable, str]]) -> Any:
|
|
|
33
34
|
return import_string
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
def get_calling_module(levels_back: int) ->
|
|
37
|
+
def get_calling_module(levels_back: int) -> ModuleType | None:
|
|
37
38
|
"""Get the module some number of stack frames back from the current one.
|
|
38
39
|
|
|
39
40
|
Make sure to account for the number of frames between the "calling" code
|
|
@@ -86,7 +87,10 @@ def seq(value, increment_by=1, start=None, suffix=None):
|
|
|
86
87
|
start = (date - epoch_datetime).total_seconds()
|
|
87
88
|
increment_by = increment_by.total_seconds()
|
|
88
89
|
for n in itertools.count(increment_by, increment_by):
|
|
89
|
-
series_date = tz_aware(
|
|
90
|
+
series_date = tz_aware(
|
|
91
|
+
datetime.datetime.fromtimestamp(start + n, tz=datetime.timezone.utc)
|
|
92
|
+
)
|
|
93
|
+
|
|
90
94
|
if type(value) is datetime.time:
|
|
91
95
|
yield series_date.time()
|
|
92
96
|
elif type(value) is datetime.date:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: model-bakery
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.21.0
|
|
4
4
|
Summary: Smart object creation facility for Django.
|
|
5
5
|
Project-URL: Homepage, https://github.com/model-bakers/model_bakery
|
|
6
6
|
Author-email: berin <bernardoxhc@gmail.com>, amureki <amureki@hey.com>
|
|
@@ -12,18 +12,20 @@ Classifier: Framework :: Django
|
|
|
12
12
|
Classifier: Framework :: Django :: 4.2
|
|
13
13
|
Classifier: Framework :: Django :: 5.0
|
|
14
14
|
Classifier: Framework :: Django :: 5.1
|
|
15
|
+
Classifier: Framework :: Django :: 5.2
|
|
16
|
+
Classifier: Framework :: Django :: 6.0
|
|
15
17
|
Classifier: Intended Audience :: Developers
|
|
16
18
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
19
|
Classifier: Operating System :: OS Independent
|
|
18
20
|
Classifier: Programming Language :: Python
|
|
19
21
|
Classifier: Programming Language :: Python :: 3
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
27
|
Classifier: Topic :: Software Development
|
|
26
|
-
Requires-Python: >=3.
|
|
28
|
+
Requires-Python: >=3.10
|
|
27
29
|
Requires-Dist: django>=4.2
|
|
28
30
|
Provides-Extra: docs
|
|
29
31
|
Requires-Dist: myst-parser; extra == 'docs'
|
|
@@ -46,76 +48,83 @@ Description-Content-Type: text/markdown
|
|
|
46
48
|
[](https://pypi.python.org/pypi/model_bakery/)
|
|
47
49
|
[](https://model-bakery.readthedocs.io/en/latest/?badge=latest)
|
|
48
50
|
|
|
49
|
-
*Model Bakery* offers you a smart way to create fixtures for testing in
|
|
50
|
-
Django.
|
|
51
|
-
With a simple and powerful API you can create many objects with a single
|
|
52
|
-
line of code.
|
|
51
|
+
*Model Bakery* offers you a smart way to create fixtures for testing in Django. With a simple and powerful API, you can create many objects with a single line of code.
|
|
53
52
|
|
|
54
|
-
Model Bakery is a rename of the legacy [Model Mommy project](https://pypi.org/project/model_mommy/).
|
|
53
|
+
> **Note:** Model Bakery is a rename of the legacy [Model Mommy project](https://pypi.org/project/model_mommy/).
|
|
55
54
|
|
|
56
|
-
##
|
|
55
|
+
## Installation
|
|
57
56
|
|
|
58
57
|
```bash
|
|
59
58
|
pip install model-bakery
|
|
60
59
|
```
|
|
61
60
|
|
|
62
|
-
##
|
|
61
|
+
## Supported Versions
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
Model Bakery follows the [Python](https://devguide.python.org/versions/) and [Django](https://docs.djangoproject.com/en/stable/internals/release-process/#supported-versions) release and support cycles. We drop support for Python and Django versions when they reach end-of-life.
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
## Basic usage
|
|
67
66
|
|
|
67
|
+
```python
|
|
68
68
|
# models.py
|
|
69
|
-
|
|
70
69
|
from django.db import models
|
|
71
70
|
|
|
72
71
|
class Customer(models.Model):
|
|
73
|
-
enjoy_jards_macale = models.BooleanField()
|
|
74
72
|
name = models.CharField(max_length=30)
|
|
75
73
|
email = models.EmailField()
|
|
76
74
|
age = models.IntegerField()
|
|
75
|
+
is_jards_macale_fan = models.BooleanField()
|
|
77
76
|
bio = models.TextField()
|
|
78
|
-
days_since_last_login = models.BigIntegerField()
|
|
79
77
|
birthday = models.DateField()
|
|
80
78
|
last_shopping = models.DateTimeField()
|
|
81
79
|
|
|
82
80
|
# test_models.py
|
|
83
|
-
|
|
84
81
|
from django.test import TestCase
|
|
85
82
|
from model_bakery import baker
|
|
86
|
-
from pprint import pprint
|
|
87
83
|
|
|
88
84
|
class TestCustomerModel(TestCase):
|
|
89
85
|
def setUp(self):
|
|
90
86
|
self.customer = baker.make('shop.Customer')
|
|
91
|
-
|
|
87
|
+
print(self.customer.__dict__)
|
|
92
88
|
|
|
93
89
|
"""
|
|
94
90
|
{'_state': <django.db.models.base.ModelState object at 0x1129a3240>,
|
|
95
91
|
'age': 3841,
|
|
96
92
|
'bio': 'vUFzMUMyKzlnTyiCxfgODIhrnkjzgQwHtzIbtnVDKflqevczfnaOACkDNqvCHwvtWdLwoiKrCqfppAlogSLECtMmfleeveyqefkGyTGnpbkVQTtviQVDESpXascHAluGHYEotSypSiHvHzFteKIcUebrzUVigiOacfnGdvijEPrZdSCIIBjuXZMaWLrMXyrsUCdKPLRBRYklRdtZhgtxuASXdhNGhDsrnPHrYRClhrSJSVFojMkUHBvSZhoXoCrTfHsAjenCEHvcLeCecsXwXgWJcnJPSFdOmOpiHRnhSgRF',
|
|
97
93
|
'birthday': datetime.date(2019, 12, 3),
|
|
98
|
-
'
|
|
94
|
+
'email': 'rpNATNsxoj@example.com',
|
|
95
|
+
'is_jards_macale_fan': True,
|
|
99
96
|
'id': 1,
|
|
100
97
|
'last_shopping': datetime.datetime(2019, 12, 3, 21, 42, 34, 77019),
|
|
101
|
-
'name': 'qiayYnESvqcYLLBzxpFOcGBIfnQEPx'
|
|
102
|
-
'days_since_last_login': 6016}
|
|
98
|
+
'name': 'qiayYnESvqcYLLBzxpFOcGBIfnQEPx'}
|
|
103
99
|
"""
|
|
104
|
-
|
|
105
100
|
```
|
|
106
101
|
|
|
107
|
-
|
|
102
|
+
## Documentation
|
|
103
|
+
|
|
104
|
+
For more detailed information, check out the [full documentation](https://model-bakery.readthedocs.io/).
|
|
108
105
|
|
|
109
106
|
## Contributing
|
|
110
107
|
|
|
111
|
-
|
|
108
|
+
As an open-source project, Model Bakery welcomes contributions of many forms:
|
|
109
|
+
|
|
110
|
+
- Code patches
|
|
111
|
+
- Documentation improvements
|
|
112
|
+
- Bug reports
|
|
113
|
+
|
|
114
|
+
Take a look at our [contribution guidelines](CONTRIBUTING.md) for instructions
|
|
115
|
+
on how to set up your local environment.
|
|
112
116
|
|
|
113
117
|
## Maintainers
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
- [Bernardo Fontes](https://github.com/berinhard/)
|
|
120
|
+
- [Rustem Saiargaliev](https://github.com/amureki/)
|
|
121
|
+
- [Tim Klein](https://github.com/timjklein36)
|
|
118
122
|
|
|
119
123
|
## Creator
|
|
120
124
|
|
|
121
|
-
|
|
125
|
+
- [Vanderson Mota](https://github.com/vandersonmota/)
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
Model Bakery is licensed under Apache License 2.0.
|
|
130
|
+
See the [LICENSE](LICENSE) file for more information.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
model_bakery/__about__.py,sha256=oQdy_VDKQZm6_UKnVHbkyIDnpqXxS3OREilFLTax8MU,23
|
|
2
|
+
model_bakery/__init__.py,sha256=yY0xZUrr_mskBm07iF-MFBEiXzOuKrlMwpKWkXZs8v0,31
|
|
3
|
+
model_bakery/_types.py,sha256=P0iKC5-Cnh3lyzyZs1mlCfOW31zbEZpxqWCuYonucKo,130
|
|
4
|
+
model_bakery/baker.py,sha256=tjsldeGJUEyIKjhIb3AXt2AjIKJ4MXZKRFJxSqH3_j0,31295
|
|
5
|
+
model_bakery/content_types.py,sha256=3POJ12aqPuSvCdKYuwb1GlJpSocgByG2JW5kw7VBNxQ,395
|
|
6
|
+
model_bakery/exceptions.py,sha256=q1oBZvfxL7HAD-F9aWgiYs4P4rz-hJ5TjeX7gcb3Q0I,333
|
|
7
|
+
model_bakery/generators.py,sha256=pV8amdkkAGbf3ES7fIiRrRyYwKHVXzIe9eN7kR_Mms0,5159
|
|
8
|
+
model_bakery/gis.py,sha256=eTNOfT98_ncLAb4KmOYJhE-3ALU2_pQbUpWPTolFZJI,1028
|
|
9
|
+
model_bakery/mock_file.txt,sha256=CO5zPS6Uydk48ZRnGcRKVEYKd2YjaUMa9QOgF5gGMCo,10
|
|
10
|
+
model_bakery/mock_img.jpeg,sha256=iF2ybgAVtDvl48-8VKdv_QFHKfnqeOaeGwR6ZLexJkY,1916
|
|
11
|
+
model_bakery/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
model_bakery/random_gen.py,sha256=A_quvJAsihqI7JP94kbJEDTX_yNa76MPIn_p2OFWdVo,11508
|
|
13
|
+
model_bakery/recipe.py,sha256=Ok0cQzX8IKpQLHjf-_kQPdlVRmSt-GPrhJsRBh7tp2g,8450
|
|
14
|
+
model_bakery/timezone.py,sha256=IvrzSer02FkUd_8DcJVOwTcRzeBn4qZ2pYvcI3QOr9E,345
|
|
15
|
+
model_bakery/utils.py,sha256=H7iASipUahE6mluEbP3cZnCkRdY4qfN3QHD7rxoYZZM,4633
|
|
16
|
+
model_bakery-1.21.0.dist-info/METADATA,sha256=771BJtN6L4f4dFuhxpSWYwa1T7Sxj92BbHwksr8tHDU,4961
|
|
17
|
+
model_bakery-1.21.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
+
model_bakery-1.21.0.dist-info/licenses/LICENSE,sha256=kCwHls7z8Y7NyZCmdnV1qLSdLrQ8bHrXLji5HOasgUc,611
|
|
19
|
+
model_bakery-1.21.0.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
model_bakery/__about__.py,sha256=V14nqouh0fns5BAgWrw5Aw8DW-D9yIY3TtnyECKUNxM,23
|
|
2
|
-
model_bakery/__init__.py,sha256=yY0xZUrr_mskBm07iF-MFBEiXzOuKrlMwpKWkXZs8v0,31
|
|
3
|
-
model_bakery/_types.py,sha256=P0iKC5-Cnh3lyzyZs1mlCfOW31zbEZpxqWCuYonucKo,130
|
|
4
|
-
model_bakery/baker.py,sha256=NSDx3hezvEG0HpWVXJd1uJPLxhJjc2tI-ipCzMbmWvw,30140
|
|
5
|
-
model_bakery/content_types.py,sha256=3POJ12aqPuSvCdKYuwb1GlJpSocgByG2JW5kw7VBNxQ,395
|
|
6
|
-
model_bakery/exceptions.py,sha256=q1oBZvfxL7HAD-F9aWgiYs4P4rz-hJ5TjeX7gcb3Q0I,333
|
|
7
|
-
model_bakery/generators.py,sha256=agY6pH14vjehVI0wJDw3Q-u4zt7mq2M1a3CUPTkAxp0,5169
|
|
8
|
-
model_bakery/gis.py,sha256=eTNOfT98_ncLAb4KmOYJhE-3ALU2_pQbUpWPTolFZJI,1028
|
|
9
|
-
model_bakery/mock_file.txt,sha256=CO5zPS6Uydk48ZRnGcRKVEYKd2YjaUMa9QOgF5gGMCo,10
|
|
10
|
-
model_bakery/mock_img.jpeg,sha256=iF2ybgAVtDvl48-8VKdv_QFHKfnqeOaeGwR6ZLexJkY,1916
|
|
11
|
-
model_bakery/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
model_bakery/random_gen.py,sha256=IYhDhqsRBftceQRHtc9B4bUWLTjvEXkgEwhNhp9DajE,9787
|
|
13
|
-
model_bakery/recipe.py,sha256=3PVg8y-vvfOf7Z4o9J7QPoO8eegGmejFCVWco19AWIs,8378
|
|
14
|
-
model_bakery/timezone.py,sha256=rd-KGVHqJoHdHKBgENPMBTRqqxgKE91CM9C0f9nS90M,325
|
|
15
|
-
model_bakery/utils.py,sha256=fw4ewtwkJ6wZ9PbDzBGvODX7CV81ozy2_6JAQmITmX4,4581
|
|
16
|
-
model_bakery-1.19.2.dist-info/METADATA,sha256=LjZgSNHtbrNUhoAHVpEwA0dgPudklKLP9jtJMkfYo0g,4355
|
|
17
|
-
model_bakery-1.19.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
18
|
-
model_bakery-1.19.2.dist-info/licenses/LICENSE,sha256=kCwHls7z8Y7NyZCmdnV1qLSdLrQ8bHrXLji5HOasgUc,611
|
|
19
|
-
model_bakery-1.19.2.dist-info/RECORD,,
|
|
File without changes
|