Skip to content

Commit 73464eb

Browse files
author
rodrigo.nogueira
committed
feat: Support nested declarations in fuzzy attributes
Recursive resolution of declarations in BaseFuzzyAttribute. Fixes #1050 - BaseFuzzyAttribute now resolves nested declarations (e.g. LazyAttribute) - Updated FuzzyChoice, FuzzyInteger, FuzzyFloat, FuzzyDate, FuzzyDateTime - Added tests for nested declarations in all fuzzy types - Updated documentation
1 parent 27bd0cc commit 73464eb

3 files changed

Lines changed: 239 additions & 58 deletions

File tree

docs/ideas.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ This is a list of future features that may be incorporated into factory_boy:
66

77
* When a :class:`~factory.Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere
88
* Define a proper set of rules for the support of third-party ORMs
9-
* Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``)
9+
* Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``) (Accomplished)

factory/fuzzy.py

Lines changed: 91 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,16 @@ class BaseFuzzyAttribute(declarations.BaseDeclaration):
2424
Custom fuzzers should override the `fuzz()` method.
2525
"""
2626

27-
def fuzz(self): # pragma: no cover
27+
def _resolve(self, value, instance, step):
28+
if isinstance(value, declarations.BaseDeclaration):
29+
return value.evaluate_pre(instance=instance, step=step, overrides={})
30+
return value
31+
32+
def fuzz(self, instance, step):
2833
raise NotImplementedError()
2934

3035
def evaluate(self, instance, step, extra):
31-
return self.fuzz()
36+
return self.fuzz(instance, step)
3237

3338

3439
class FuzzyAttribute(BaseFuzzyAttribute):
@@ -43,7 +48,7 @@ def __init__(self, fuzzer):
4348
super().__init__()
4449
self.fuzzer = fuzzer
4550

46-
def fuzz(self):
51+
def fuzz(self, instance, step):
4752
return self.fuzzer()
4853

4954

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

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

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

94-
def fuzz(self):
99+
def fuzz(self, instance, step):
95100
if self.choices is None:
96-
self.choices = list(self.choices_generator)
101+
resolved = self._resolve(self.choices_generator, instance, step)
102+
self.choices = list(resolved)
97103
value = random.randgen.choice(self.choices)
98104
if self.getter is None:
99105
return value
@@ -104,55 +110,55 @@ class FuzzyInteger(BaseFuzzyAttribute):
104110
"""Random integer within a given range."""
105111

106112
def __init__(self, low, high=None, step=1):
107-
if high is None:
108-
high = low
109-
low = 0
110-
111113
self.low = low
112114
self.high = high
113115
self.step = step
114-
115116
super().__init__()
116117

117-
def fuzz(self):
118-
return random.randgen.randrange(self.low, self.high + 1, self.step)
118+
def fuzz(self, instance, step):
119+
low = self._resolve(self.low, instance, step)
120+
high = self._resolve(self.high, instance, step)
121+
if high is None:
122+
high = low
123+
low = 0
124+
return random.randgen.randrange(low, high + 1, self.step)
119125

120126

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

124130
def __init__(self, low, high=None, precision=2):
125-
if high is None:
126-
high = low
127-
low = 0.0
128-
129131
self.low = low
130132
self.high = high
131133
self.precision = precision
132-
133134
super().__init__()
134135

135-
def fuzz(self):
136-
base = decimal.Decimal(str(random.randgen.uniform(self.low, self.high)))
136+
def fuzz(self, instance, step):
137+
low = self._resolve(self.low, instance, step)
138+
high = self._resolve(self.high, instance, step)
139+
if high is None:
140+
high = low
141+
low = 0.0
142+
base = decimal.Decimal(str(random.randgen.uniform(low, high)))
137143
return base.quantize(decimal.Decimal(10) ** -self.precision)
138144

139145

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

143149
def __init__(self, low, high=None, precision=15):
144-
if high is None:
145-
high = low
146-
low = 0
147-
148150
self.low = low
149151
self.high = high
150152
self.precision = precision
151-
152153
super().__init__()
153154

154-
def fuzz(self):
155-
base = random.randgen.uniform(self.low, self.high)
155+
def fuzz(self, instance, step):
156+
low = self._resolve(self.low, instance, step)
157+
high = self._resolve(self.high, instance, step)
158+
if high is None:
159+
high = low
160+
low = 0
161+
base = random.randgen.uniform(low, high)
156162
return float(format(base, '.%dg' % self.precision))
157163

158164

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

162168
def __init__(self, start_date, end_date=None):
163169
super().__init__()
164-
if end_date is None:
165-
if random.randgen.state_set:
166-
cls_name = self.__class__.__name__
167-
warnings.warn(random_seed_warning.format(cls_name), stacklevel=2)
168-
end_date = datetime.date.today()
169-
170-
if start_date > end_date:
171-
raise ValueError(
172-
"FuzzyDate boundaries should have start <= end; got %r > %r."
173-
% (start_date, end_date))
174-
175-
self.start_date = start_date.toordinal()
176-
self.end_date = end_date.toordinal()
177-
178-
def fuzz(self):
179-
return datetime.date.fromordinal(random.randgen.randint(self.start_date, self.end_date))
170+
self._start_is_decl = isinstance(start_date, declarations.BaseDeclaration)
171+
self._end_is_decl = isinstance(end_date, declarations.BaseDeclaration)
172+
self.start_date = start_date
173+
self.end_date = end_date
174+
if not self._start_is_decl and not self._end_is_decl:
175+
if end_date is None:
176+
if random.randgen.state_set:
177+
cls_name = self.__class__.__name__
178+
warnings.warn(random_seed_warning.format(cls_name), stacklevel=2)
179+
end_date = datetime.date.today()
180+
self.end_date = end_date
181+
if start_date > end_date:
182+
raise ValueError(
183+
"FuzzyDate boundaries should have start <= end; got %r > %r."
184+
% (start_date, end_date))
185+
self._start_ord = start_date.toordinal()
186+
self._end_ord = end_date.toordinal()
187+
188+
def fuzz(self, instance, step):
189+
if self._start_is_decl or self._end_is_decl:
190+
start_date = self._resolve(self.start_date, instance, step)
191+
end_date = self._resolve(self.end_date, instance, step)
192+
if end_date is None:
193+
end_date = datetime.date.today()
194+
if start_date > end_date:
195+
raise ValueError(
196+
"FuzzyDate boundaries should have start <= end; got %r > %r."
197+
% (start_date, end_date))
198+
start_ord = start_date.toordinal()
199+
end_ord = end_date.toordinal()
200+
else:
201+
start_ord = self._start_ord
202+
end_ord = self._end_ord
203+
return datetime.date.fromordinal(random.randgen.randint(start_ord, end_ord))
180204

181205

182206
class BaseFuzzyDateTime(BaseFuzzyAttribute):
@@ -199,15 +223,8 @@ def __init__(self, start_dt, end_dt=None,
199223
force_hour=None, force_minute=None, force_second=None,
200224
force_microsecond=None):
201225
super().__init__()
202-
203-
if end_dt is None:
204-
if random.randgen.state_set:
205-
cls_name = self.__class__.__name__
206-
warnings.warn(random_seed_warning.format(cls_name), stacklevel=2)
207-
end_dt = self._now()
208-
209-
self._check_bounds(start_dt, end_dt)
210-
226+
self._start_is_decl = isinstance(start_dt, declarations.BaseDeclaration)
227+
self._end_is_decl = isinstance(end_dt, declarations.BaseDeclaration)
211228
self.start_dt = start_dt
212229
self.end_dt = end_dt
213230
self.force_year = force_year
@@ -217,13 +234,31 @@ def __init__(self, start_dt, end_dt=None,
217234
self.force_minute = force_minute
218235
self.force_second = force_second
219236
self.force_microsecond = force_microsecond
220-
221-
def fuzz(self):
222-
delta = self.end_dt - self.start_dt
237+
if not self._start_is_decl and not self._end_is_decl:
238+
if end_dt is None:
239+
if random.randgen.state_set:
240+
cls_name = self.__class__.__name__
241+
warnings.warn(random_seed_warning.format(cls_name), stacklevel=2)
242+
end_dt = self._now()
243+
self.end_dt = end_dt
244+
self._check_bounds(start_dt, end_dt)
245+
246+
def fuzz(self, instance, step):
247+
if self._start_is_decl or self._end_is_decl:
248+
start_dt = self._resolve(self.start_dt, instance, step)
249+
end_dt = self._resolve(self.end_dt, instance, step)
250+
if end_dt is None:
251+
end_dt = self._now()
252+
self._check_bounds(start_dt, end_dt)
253+
else:
254+
start_dt = self.start_dt
255+
end_dt = self.end_dt
256+
257+
delta = end_dt - start_dt
223258
microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400))
224259

225260
offset = random.randgen.randint(0, microseconds)
226-
result = self.start_dt + datetime.timedelta(microseconds=offset)
261+
result = start_dt + datetime.timedelta(microseconds=offset)
227262

228263
if self.force_year is not None:
229264
result = result.replace(year=self.force_year)

0 commit comments

Comments
 (0)