Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/ideas.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ This is a list of future features that may be incorporated into factory_boy:

* When a :class:`~factory.Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere
* Define a proper set of rules for the support of third-party ORMs
* Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``)
* Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``) (Accomplished)
147 changes: 91 additions & 56 deletions factory/fuzzy.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ class BaseFuzzyAttribute(declarations.BaseDeclaration):
Custom fuzzers should override the `fuzz()` method.
"""

def fuzz(self): # pragma: no cover
def _resolve(self, value, instance, step):
if isinstance(value, declarations.BaseDeclaration):
return value.evaluate_pre(instance=instance, step=step, overrides={})
return value

def fuzz(self, instance, step):
raise NotImplementedError()

def evaluate(self, instance, step, extra):
return self.fuzz()
return self.fuzz(instance, step)


class FuzzyAttribute(BaseFuzzyAttribute):
Expand All @@ -43,7 +48,7 @@ def __init__(self, fuzzer):
super().__init__()
self.fuzzer = fuzzer

def fuzz(self):
def fuzz(self, instance, step):
return self.fuzzer()


Expand Down Expand Up @@ -71,7 +76,7 @@ def __init__(self, prefix='', length=12, suffix='', chars=string.ascii_letters):
self.length = length
self.chars = tuple(chars) # Unroll iterators

def fuzz(self):
def fuzz(self, instance, step):
chars = [random.randgen.choice(self.chars) for _i in range(self.length)]
return self.prefix + ''.join(chars) + self.suffix

Expand All @@ -91,9 +96,10 @@ def __init__(self, choices, getter=None):
self.getter = getter
super().__init__()

def fuzz(self):
def fuzz(self, instance, step):
if self.choices is None:
self.choices = list(self.choices_generator)
resolved = self._resolve(self.choices_generator, instance, step)
self.choices = list(resolved)
value = random.randgen.choice(self.choices)
if self.getter is None:
return value
Expand All @@ -104,55 +110,55 @@ class FuzzyInteger(BaseFuzzyAttribute):
"""Random integer within a given range."""

def __init__(self, low, high=None, step=1):
if high is None:
high = low
low = 0

self.low = low
self.high = high
self.step = step

super().__init__()

def fuzz(self):
return random.randgen.randrange(self.low, self.high + 1, self.step)
def fuzz(self, instance, step):
low = self._resolve(self.low, instance, step)
high = self._resolve(self.high, instance, step)
if high is None:
high = low
low = 0
return random.randgen.randrange(low, high + 1, self.step)


class FuzzyDecimal(BaseFuzzyAttribute):
"""Random decimal within a given range."""

def __init__(self, low, high=None, precision=2):
if high is None:
high = low
low = 0.0

self.low = low
self.high = high
self.precision = precision

super().__init__()

def fuzz(self):
base = decimal.Decimal(str(random.randgen.uniform(self.low, self.high)))
def fuzz(self, instance, step):
low = self._resolve(self.low, instance, step)
high = self._resolve(self.high, instance, step)
if high is None:
high = low
low = 0.0
base = decimal.Decimal(str(random.randgen.uniform(low, high)))
return base.quantize(decimal.Decimal(10) ** -self.precision)


class FuzzyFloat(BaseFuzzyAttribute):
"""Random float within a given range."""

def __init__(self, low, high=None, precision=15):
if high is None:
high = low
low = 0

self.low = low
self.high = high
self.precision = precision

super().__init__()

def fuzz(self):
base = random.randgen.uniform(self.low, self.high)
def fuzz(self, instance, step):
low = self._resolve(self.low, instance, step)
high = self._resolve(self.high, instance, step)
if high is None:
high = low
low = 0
base = random.randgen.uniform(low, high)
return float(format(base, '.%dg' % self.precision))


Expand All @@ -161,22 +167,40 @@ class FuzzyDate(BaseFuzzyAttribute):

def __init__(self, start_date, end_date=None):
super().__init__()
if end_date is None:
if random.randgen.state_set:
cls_name = self.__class__.__name__
warnings.warn(random_seed_warning.format(cls_name), stacklevel=2)
end_date = datetime.date.today()

if start_date > end_date:
raise ValueError(
"FuzzyDate boundaries should have start <= end; got %r > %r."
% (start_date, end_date))

self.start_date = start_date.toordinal()
self.end_date = end_date.toordinal()

def fuzz(self):
return datetime.date.fromordinal(random.randgen.randint(self.start_date, self.end_date))
self._start_is_decl = isinstance(start_date, declarations.BaseDeclaration)
self._end_is_decl = isinstance(end_date, declarations.BaseDeclaration)
self.start_date = start_date
self.end_date = end_date
if not self._start_is_decl and not self._end_is_decl:
if end_date is None:
if random.randgen.state_set:
cls_name = self.__class__.__name__
warnings.warn(random_seed_warning.format(cls_name), stacklevel=2)
end_date = datetime.date.today()
self.end_date = end_date
if start_date > end_date:
raise ValueError(
"FuzzyDate boundaries should have start <= end; got %r > %r."
% (start_date, end_date))
self._start_ord = start_date.toordinal()
self._end_ord = end_date.toordinal()

def fuzz(self, instance, step):
if self._start_is_decl or self._end_is_decl:
start_date = self._resolve(self.start_date, instance, step)
end_date = self._resolve(self.end_date, instance, step)
if end_date is None:
end_date = datetime.date.today()
if start_date > end_date:
raise ValueError(
"FuzzyDate boundaries should have start <= end; got %r > %r."
% (start_date, end_date))
start_ord = start_date.toordinal()
end_ord = end_date.toordinal()
else:
start_ord = self._start_ord
end_ord = self._end_ord
return datetime.date.fromordinal(random.randgen.randint(start_ord, end_ord))


class BaseFuzzyDateTime(BaseFuzzyAttribute):
Expand All @@ -199,15 +223,8 @@ def __init__(self, start_dt, end_dt=None,
force_hour=None, force_minute=None, force_second=None,
force_microsecond=None):
super().__init__()

if end_dt is None:
if random.randgen.state_set:
cls_name = self.__class__.__name__
warnings.warn(random_seed_warning.format(cls_name), stacklevel=2)
end_dt = self._now()

self._check_bounds(start_dt, end_dt)

self._start_is_decl = isinstance(start_dt, declarations.BaseDeclaration)
self._end_is_decl = isinstance(end_dt, declarations.BaseDeclaration)
self.start_dt = start_dt
self.end_dt = end_dt
self.force_year = force_year
Expand All @@ -217,13 +234,31 @@ def __init__(self, start_dt, end_dt=None,
self.force_minute = force_minute
self.force_second = force_second
self.force_microsecond = force_microsecond

def fuzz(self):
delta = self.end_dt - self.start_dt
if not self._start_is_decl and not self._end_is_decl:
if end_dt is None:
if random.randgen.state_set:
cls_name = self.__class__.__name__
warnings.warn(random_seed_warning.format(cls_name), stacklevel=2)
end_dt = self._now()
self.end_dt = end_dt
self._check_bounds(start_dt, end_dt)

def fuzz(self, instance, step):
if self._start_is_decl or self._end_is_decl:
start_dt = self._resolve(self.start_dt, instance, step)
end_dt = self._resolve(self.end_dt, instance, step)
if end_dt is None:
end_dt = self._now()
self._check_bounds(start_dt, end_dt)
else:
start_dt = self.start_dt
end_dt = self.end_dt

delta = end_dt - start_dt
microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400))

offset = random.randgen.randint(0, microseconds)
result = self.start_dt + datetime.timedelta(microseconds=offset)
result = start_dt + datetime.timedelta(microseconds=offset)

if self.force_year is not None:
result = result.replace(year=self.force_year)
Expand Down
Loading