Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 9 additions & 19 deletions model_bakery/generators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from decimal import Decimal
from typing import Any, Callable, Dict, Optional, Type, Union

from django.db.backends.base.operations import BaseDatabaseOperations
from django.db.models import (
AutoField,
BigAutoField,
Expand Down Expand Up @@ -81,29 +80,20 @@
IntegerRangeField = None


def _make_integer_gen_by_range(field_type: Any) -> Callable:
min_int, max_int = BaseDatabaseOperations.integer_field_ranges[field_type.__name__]

def gen_integer():
return random_gen.gen_integer(min_int=min_int, max_int=max_int)

return gen_integer


default_mapping = {
ForeignKey: random_gen.gen_related,
OneToOneField: random_gen.gen_related,
ManyToManyField: random_gen.gen_m2m,
BooleanField: random_gen.gen_boolean,
AutoField: _make_integer_gen_by_range(AutoField),
BigAutoField: _make_integer_gen_by_range(BigAutoField),
IntegerField: _make_integer_gen_by_range(IntegerField),
SmallAutoField: _make_integer_gen_by_range(SmallAutoField),
BigIntegerField: _make_integer_gen_by_range(BigIntegerField),
SmallIntegerField: _make_integer_gen_by_range(SmallIntegerField),
PositiveBigIntegerField: _make_integer_gen_by_range(PositiveBigIntegerField),
PositiveIntegerField: _make_integer_gen_by_range(PositiveIntegerField),
PositiveSmallIntegerField: _make_integer_gen_by_range(PositiveSmallIntegerField),
AutoField: random_gen.gen_auto_field,
BigAutoField: random_gen.gen_positive_big_integer,
IntegerField: random_gen.gen_regular_integer,
SmallAutoField: random_gen.gen_positive_small_integer,
BigIntegerField: random_gen.gen_big_integer,
SmallIntegerField: random_gen.gen_small_integer,
PositiveBigIntegerField: random_gen.gen_positive_big_integer,
PositiveIntegerField: random_gen.gen_positive_integer,
PositiveSmallIntegerField: random_gen.gen_positive_small_integer,
FloatField: random_gen.gen_float,
DecimalField: random_gen.gen_decimal,
BinaryField: random_gen.gen_byte_string,
Expand Down
78 changes: 78 additions & 0 deletions model_bakery/random_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,87 @@ def gen_from_choices(choices: List) -> Callable:


def gen_integer(min_int: int = -MAX_INT, max_int: int = MAX_INT) -> int:
warnings.warn(
"gen_integer() may cause overflow errors with Django integer fields due to "
"large default MAX_INT value. Consider using field-specific generators instead:\n"
"- gen_positive_small_integer() for PositiveSmallIntegerField\n"
"- gen_small_integer() for SmallIntegerField\n"
"- gen_regular_integer() for IntegerField\n"
"- gen_positive_integer() for PositiveIntegerField\n"
"- gen_big_integer() for BigIntegerField\n"
"- gen_positive_big_integer() for PositiveBigIntegerField\n"
"See model_bakery.random_gen documentation for more details.",
DeprecationWarning,
stacklevel=2,
)
Comment on lines +80 to +92
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice touch with the warning 👍

return baker_random.randint(min_int, max_int)


def _get_field_range(field_name: str):
"""Get field range from Django's BaseDatabaseOperations."""
from django.db.backends.base.operations import BaseDatabaseOperations

return BaseDatabaseOperations.integer_field_ranges.get(
field_name, (-MAX_INT, MAX_INT)
)
Comment on lines +98 to +102
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice finding!



def gen_small_integer(min_int: int = None, max_int: int = None) -> int:
"""Generate integer for SmallIntegerField."""
field_min, field_max = _get_field_range("SmallIntegerField")
actual_min = min_int if min_int is not None else field_min
actual_max = max_int if max_int is not None else field_max
return baker_random.randint(actual_min, actual_max)


def gen_positive_small_integer(min_int: int = None, max_int: int = None) -> int:
"""Generate integer for PositiveSmallIntegerField."""
field_min, field_max = _get_field_range("PositiveSmallIntegerField")
actual_min = min_int if min_int is not None else max(field_min, 1)
actual_max = max_int if max_int is not None else field_max
return baker_random.randint(actual_min, actual_max)


def gen_positive_integer(min_int: int = None, max_int: int = None) -> int:
"""Generate integer for PositiveIntegerField."""
field_min, field_max = _get_field_range("PositiveIntegerField")
actual_min = min_int if min_int is not None else max(field_min, 1)
actual_max = max_int if max_int is not None else field_max
return baker_random.randint(actual_min, actual_max)


def gen_big_integer(min_int: int = None, max_int: int = None) -> int:
"""Generate integer for BigIntegerField."""
field_min, field_max = _get_field_range("BigIntegerField")
actual_min = min_int if min_int is not None else field_min
actual_max = max_int if max_int is not None else field_max
return baker_random.randint(actual_min, actual_max)


def gen_positive_big_integer(min_int: int = None, max_int: int = None) -> int:
"""Generate integer for PositiveBigIntegerField."""
field_min, field_max = _get_field_range("PositiveBigIntegerField")
actual_min = min_int if min_int is not None else max(field_min, 1)
actual_max = max_int if max_int is not None else field_max
return baker_random.randint(actual_min, actual_max)


def gen_regular_integer(min_int: int = None, max_int: int = None) -> int:
"""Generate integer for IntegerField."""
field_min, field_max = _get_field_range("IntegerField")
actual_min = min_int if min_int is not None else field_min
actual_max = max_int if max_int is not None else field_max
return baker_random.randint(actual_min, actual_max)


def gen_auto_field(min_int: int = None, max_int: int = None) -> int:
"""Generate integer for AutoField."""
field_min, field_max = _get_field_range("AutoField")
actual_min = min_int if min_int is not None else max(field_min, 1)
actual_max = max_int if max_int is not None else field_max
return baker_random.randint(actual_min, actual_max)


def gen_float() -> float:
return baker_random.random() * gen_integer()

Expand Down
44 changes: 44 additions & 0 deletions tests/test_baker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1184,3 +1184,47 @@ def test_make_with_auto_now_and_fill_optional(self):
assert instance.created == created
assert instance.updated == updated
assert instance.sent_date == sent_date


class TestFieldSpecificIntegerGenerators:
@pytest.mark.django_db
def test_gen_positive_small_integer_works_safely(self):
obj = baker.make(
models.DummyPositiveIntModel,
positive_small_int_field=random_gen.gen_positive_small_integer(min_int=1),
)

assert 1 <= obj.positive_small_int_field <= 32767

def test_field_specific_generators_respect_constraints(self):
obj = baker.make(
models.DummyPositiveIntModel,
positive_small_int_field=random_gen.gen_positive_small_integer(
min_int=100, max_int=200
),
)

assert 100 <= obj.positive_small_int_field <= 200

def test_gen_integer_shows_deprecation_warning(self):
import warnings

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")

# This should trigger the deprecation warning
value = random_gen.gen_integer(min_int=1, max_int=100)

assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert "gen_integer() may cause overflow errors" in str(w[0].message)
assert "gen_positive_small_integer()" in str(w[0].message)

assert 1 <= value <= 100

def test_automatic_generation_still_works(self):
obj = baker.make(models.DummyPositiveIntModel)

assert 1 <= obj.positive_small_int_field <= 32767
assert isinstance(obj.positive_int_field, int)
assert isinstance(obj.positive_big_int_field, int)
Loading