4
4
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
5
5
from distutils .version import LooseVersion
6
6
from functools import partial
7
+ import weakref
7
8
8
9
try : # python 3.3+
9
10
from inspect import signature
@@ -35,7 +36,7 @@ def get_id(self):
35
36
raise NotImplementedError ()
36
37
37
38
# @abstractmethod
38
- def get (self ):
39
+ def get (self , request ):
39
40
"""Return the value to use by pytest"""
40
41
raise NotImplementedError ()
41
42
@@ -123,14 +124,21 @@ class _LazyValue(Lazy):
123
124
124
125
A `lazy_value` is the same thing than a function-scoped fixture, except that the value getter function is not a
125
126
fixture and therefore can neither be parametrized nor depend on fixtures. It should have no mandatory argument.
127
+
128
+ The `self.get(request)` method can be used to get the value for the current pytest context. This value will
129
+ be cached so that plugins can call it several time without triggering new calls to the underlying function.
130
+ So the underlying function will be called exactly once per test node.
131
+
132
+ See https://github.com/smarie/python-pytest-cases/issues/149
133
+ and https://github.com/smarie/python-pytest-cases/issues/143
126
134
"""
127
135
if pytest53 :
128
- __slots__ = 'valuegetter' , '_id' , '_marks' , 'retrieved ' , 'value '
136
+ __slots__ = 'valuegetter' , '_id' , '_marks' , 'cached_value_context ' , 'cached_value '
129
137
_field_names = __slots__
130
138
else :
131
139
# we can not define __slots__ since we'll extend int in a subclass
132
140
# see https://docs.python.org/3/reference/datamodel.html?highlight=__slots__#notes-on-using-slots
133
- _field_names = 'valuegetter' , '_id' , '_marks' , 'retrieved ' , 'value '
141
+ _field_names = 'valuegetter' , '_id' , '_marks' , 'cached_value_context ' , 'cached_value '
134
142
135
143
@classmethod
136
144
def copy_from (cls ,
@@ -139,9 +147,9 @@ def copy_from(cls,
139
147
"""Creates a copy of this _LazyValue"""
140
148
new_obj = cls (valuegetter = obj .valuegetter , id = obj ._id , marks = obj ._marks )
141
149
# make sure the copy will not need to retrieve the result if already done
142
- new_obj . retrieved = obj .retrieved
143
- if new_obj .retrieved :
144
- new_obj .value = obj .value
150
+ if obj .has_cached_value ():
151
+ new_obj .cached_value_context = obj . cached_value_context
152
+ new_obj .cached_value = obj .cached_value
145
153
return new_obj
146
154
147
155
# noinspection PyMissingConstructor
@@ -156,8 +164,8 @@ def __init__(self,
156
164
self ._marks = marks
157
165
else :
158
166
self ._marks = (marks , )
159
- self .retrieved = False
160
- self .value = None
167
+ self .cached_value_context = None
168
+ self .cached_value = None
161
169
162
170
def get_marks (self , as_decorators = False ):
163
171
"""
@@ -192,22 +200,32 @@ def get_id(self):
192
200
else :
193
201
return vg .__name__
194
202
195
- def get (self ):
196
- """ Call the underlying value getter, then return the result value (not self). With a cache mechanism """
197
- if not self .retrieved :
198
- # retrieve
199
- self .value = self .valuegetter ()
200
- self .retrieved = True
203
+ def get (self , request ):
204
+ """
205
+ Calls the underlying value getter function `self.valuegetter` and returns the result.
206
+
207
+ This result is cached to ensure that the underlying getter function is called exactly once for each
208
+ pytest node. Note that we do not cache across calls to preserve the pytest spirit of "no leakage
209
+ across test nodes" especially when the value is mutable.
210
+
211
+ See https://github.com/smarie/python-pytest-cases/issues/149
212
+ and https://github.com/smarie/python-pytest-cases/issues/143
213
+ """
214
+ if self .cached_value_context is None or self .cached_value_context () is not request .node :
215
+ # retrieve the value by calling the function
216
+ self .cached_value = self .valuegetter ()
217
+ # remember the pytest context of the call with a weak reference to avoir gc issues
218
+ self .cached_value_context = weakref .ref (request .node )
219
+
220
+ return self .cached_value
201
221
202
- return self .value
222
+ def has_cached_value (self ):
223
+ """Return True if there is a cached value in self.value, but with no guarantee that it corresponds to the
224
+ current request"""
225
+ return self .cached_value_context is not None
203
226
204
227
def as_lazy_tuple (self , nb_params ):
205
- res = LazyTuple (self , nb_params )
206
- if self .retrieved :
207
- # make sure the tuple will not need to retrieve the result if already done
208
- res .retrieved = True
209
- res .value = self .value
210
- return res
228
+ return LazyTuple (self , nb_params )
211
229
212
230
def as_lazy_items_list (self , nb_params ):
213
231
return [v for v in self .as_lazy_tuple (nb_params )]
@@ -244,15 +262,15 @@ def __repr__(self):
244
262
"""Override the inherited method to avoid infinite recursion"""
245
263
vals_to_display = (
246
264
('item' , self .item ), # item number first for easier debug
247
- ('tuple' , self .host .value if self .host .retrieved else self .host .valuegetter ), # lazy value tuple or retrieved tuple
265
+ ('tuple' , self .host .cached_value if self .host .has_cached_value () else self .host ._lazyvalue ), # lazy value tuple or cached tuple
248
266
)
249
267
return "%s(%s)" % (self .__class__ .__name__ , ", " .join ("%s=%r" % (k , v ) for k , v in vals_to_display ))
250
268
251
269
def get_id (self ):
252
270
return "%s[%s]" % (self .host .get_id (), self .item )
253
271
254
- def get (self ):
255
- return self .host .force_getitem (self .item )
272
+ def get (self , request ):
273
+ return self .host .force_getitem (self .item , request )
256
274
257
275
258
276
class LazyTuple (Lazy ):
@@ -268,70 +286,68 @@ class LazyTuple(Lazy):
268
286
In all other cases (when @parametrize is used on a test function), pytest unpacks the tuple so it directly
269
287
manipulates the underlying LazyTupleItem instances.
270
288
"""
271
- __slots__ = 'valuegetter ' , 'theoretical_size' , 'retrieved' , 'value '
289
+ __slots__ = '_lazyvalue ' , 'theoretical_size'
272
290
_field_names = __slots__
273
291
274
292
@classmethod
275
293
def copy_from (cls ,
276
294
obj # type: LazyTuple
277
295
):
278
- new_obj = cls (valueref = obj .value , theoretical_size = obj .theoretical_size )
279
- # make sure the copy will not need to retrieve the result if already done
280
- new_obj .retrieved = obj .retrieved
281
- if new_obj .retrieved :
282
- new_obj .value = obj .value
283
- return new_obj
296
+ # clone the inner lazy value
297
+ value_copy = obj ._lazyvalue .clone ()
298
+ return cls (valueref = value_copy , theoretical_size = obj .theoretical_size )
284
299
285
300
# noinspection PyMissingConstructor
286
301
def __init__ (self ,
287
- valueref , # type: Union[LazyValue, Sequence]
302
+ valueref , # type: _LazyValue
288
303
theoretical_size # type: int
289
304
):
290
- self .valuegetter = valueref
305
+ self ._lazyvalue = valueref
291
306
self .theoretical_size = theoretical_size
292
- self .retrieved = False
293
- self .value = None
294
307
295
308
def __len__ (self ):
296
309
return self .theoretical_size
297
310
298
311
def get_id (self ):
299
312
"""return the id to use by pytest"""
300
- return self .valuegetter .get_id ()
313
+ return self ._lazyvalue .get_id ()
301
314
302
- def get (self ):
303
- """ Call the underlying value getter, then return the result tuple (not self). With a cache mechanism """
304
- if not self .retrieved :
305
- # retrieve
306
- self .value = self .valuegetter .get ()
307
- self .retrieved = True
308
- return self .value
315
+ def get (self , request ):
316
+ """ Call the underlying value getter, then return the result tuple value (not self). """
317
+ return self ._lazyvalue .get (request )
318
+
319
+ def has_cached_value (self ):
320
+ return self ._lazyvalue .has_cached_value ()
321
+
322
+ @property
323
+ def cached_value (self ):
324
+ return self ._lazyvalue .cached_value
309
325
310
326
def __getitem__ (self , item ):
311
327
"""
312
328
Getting an item in the tuple with self[i] does *not* retrieve the value automatically, but returns
313
329
a facade (a LazyTupleItem), so that pytest can store this item independently wherever needed, without
314
330
yet calling the value getter.
315
331
"""
316
- if self .retrieved :
332
+ if self ._lazyvalue . has_cached_value () :
317
333
# this is never called by pytest, but keep it for debugging
318
- return self .value [item ]
334
+ return self ._lazyvalue . cached_value [item ]
319
335
elif item >= self .theoretical_size :
320
336
raise IndexError (item )
321
337
else :
322
338
# do not retrieve yet: return a facade
323
339
return LazyTupleItem (self , item )
324
340
325
- def force_getitem (self , item ):
341
+ def force_getitem (self , item , request ):
326
342
""" Call the underlying value getter, then return self[i]. """
327
- argvalue = self .get ()
343
+ argvalue = self .get (request )
328
344
try :
329
345
return argvalue [item ]
330
346
except TypeError as e :
331
347
raise ValueError ("(lazy_value) The parameter value returned by `%r` is not compliant with the number"
332
348
" of argnames in parametrization (%s). A %s-tuple-like was expected. "
333
349
"Returned lazy argvalue is %r and argvalue[%s] raised %s: %s"
334
- % (self .valuegetter , self .theoretical_size , self .theoretical_size ,
350
+ % (self ._lazyvalue , self .theoretical_size , self .theoretical_size ,
335
351
argvalue , item , e .__class__ , e ))
336
352
337
353
@@ -402,6 +418,7 @@ def lazy_value(valuegetter, # type: Callable[[], Any]
402
418
403
419
A `lazy_value` is the same thing than a function-scoped fixture, except that the value getter function is not a
404
420
fixture and therefore can neither be parametrized nor depend on fixtures. It should have no mandatory argument.
421
+ The underlying function will be called exactly once per test node.
405
422
406
423
By default the associated id is the name of the `valuegetter` callable, but a specific `id` can be provided
407
424
otherwise. Note that this `id` does not take precedence over custom `ids` or `idgen` passed to @parametrize.
@@ -439,15 +456,19 @@ def is_lazy(argval):
439
456
return False
440
457
441
458
442
- def get_lazy_args (argval ):
443
- """ Possibly calls the lazy values contained in argval if needed, before returning it"""
459
+ def get_lazy_args (argval , request ):
460
+ """
461
+ Possibly calls the lazy values contained in argval if needed, before returning it.
462
+ Since the lazy values cache their result to ensure that their underlying function is called only once
463
+ per test node, the `request` argument here is mandatory.
464
+ """
444
465
445
466
try :
446
467
_is_lazy = is_lazy (argval )
447
468
except : # noqa
448
469
return argval
449
470
else :
450
471
if _is_lazy :
451
- return argval .get ()
472
+ return argval .get (request )
452
473
else :
453
474
return argval
0 commit comments