44# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
55from distutils .version import LooseVersion
66from functools import partial
7+ import weakref
78
89try : # python 3.3+
910 from inspect import signature
@@ -35,7 +36,7 @@ def get_id(self):
3536 raise NotImplementedError ()
3637
3738 # @abstractmethod
38- def get (self ):
39+ def get (self , request ):
3940 """Return the value to use by pytest"""
4041 raise NotImplementedError ()
4142
@@ -123,14 +124,21 @@ class _LazyValue(Lazy):
123124
124125 A `lazy_value` is the same thing than a function-scoped fixture, except that the value getter function is not a
125126 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
126134 """
127135 if pytest53 :
128- __slots__ = 'valuegetter' , '_id' , '_marks' , 'retrieved ' , 'value '
136+ __slots__ = 'valuegetter' , '_id' , '_marks' , 'cached_value_context ' , 'cached_value '
129137 _field_names = __slots__
130138 else :
131139 # we can not define __slots__ since we'll extend int in a subclass
132140 # 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 '
134142
135143 @classmethod
136144 def copy_from (cls ,
@@ -139,9 +147,9 @@ def copy_from(cls,
139147 """Creates a copy of this _LazyValue"""
140148 new_obj = cls (valuegetter = obj .valuegetter , id = obj ._id , marks = obj ._marks )
141149 # 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
145153 return new_obj
146154
147155 # noinspection PyMissingConstructor
@@ -156,8 +164,8 @@ def __init__(self,
156164 self ._marks = marks
157165 else :
158166 self ._marks = (marks , )
159- self .retrieved = False
160- self .value = None
167+ self .cached_value_context = None
168+ self .cached_value = None
161169
162170 def get_marks (self , as_decorators = False ):
163171 """
@@ -192,22 +200,32 @@ def get_id(self):
192200 else :
193201 return vg .__name__
194202
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
201221
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
203226
204227 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 )
211229
212230 def as_lazy_items_list (self , nb_params ):
213231 return [v for v in self .as_lazy_tuple (nb_params )]
@@ -244,15 +262,15 @@ def __repr__(self):
244262 """Override the inherited method to avoid infinite recursion"""
245263 vals_to_display = (
246264 ('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
248266 )
249267 return "%s(%s)" % (self .__class__ .__name__ , ", " .join ("%s=%r" % (k , v ) for k , v in vals_to_display ))
250268
251269 def get_id (self ):
252270 return "%s[%s]" % (self .host .get_id (), self .item )
253271
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 )
256274
257275
258276class LazyTuple (Lazy ):
@@ -268,70 +286,68 @@ class LazyTuple(Lazy):
268286 In all other cases (when @parametrize is used on a test function), pytest unpacks the tuple so it directly
269287 manipulates the underlying LazyTupleItem instances.
270288 """
271- __slots__ = 'valuegetter ' , 'theoretical_size' , 'retrieved' , 'value '
289+ __slots__ = '_lazyvalue ' , 'theoretical_size'
272290 _field_names = __slots__
273291
274292 @classmethod
275293 def copy_from (cls ,
276294 obj # type: LazyTuple
277295 ):
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 )
284299
285300 # noinspection PyMissingConstructor
286301 def __init__ (self ,
287- valueref , # type: Union[LazyValue, Sequence]
302+ valueref , # type: _LazyValue
288303 theoretical_size # type: int
289304 ):
290- self .valuegetter = valueref
305+ self ._lazyvalue = valueref
291306 self .theoretical_size = theoretical_size
292- self .retrieved = False
293- self .value = None
294307
295308 def __len__ (self ):
296309 return self .theoretical_size
297310
298311 def get_id (self ):
299312 """return the id to use by pytest"""
300- return self .valuegetter .get_id ()
313+ return self ._lazyvalue .get_id ()
301314
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
309325
310326 def __getitem__ (self , item ):
311327 """
312328 Getting an item in the tuple with self[i] does *not* retrieve the value automatically, but returns
313329 a facade (a LazyTupleItem), so that pytest can store this item independently wherever needed, without
314330 yet calling the value getter.
315331 """
316- if self .retrieved :
332+ if self ._lazyvalue . has_cached_value () :
317333 # this is never called by pytest, but keep it for debugging
318- return self .value [item ]
334+ return self ._lazyvalue . cached_value [item ]
319335 elif item >= self .theoretical_size :
320336 raise IndexError (item )
321337 else :
322338 # do not retrieve yet: return a facade
323339 return LazyTupleItem (self , item )
324340
325- def force_getitem (self , item ):
341+ def force_getitem (self , item , request ):
326342 """ Call the underlying value getter, then return self[i]. """
327- argvalue = self .get ()
343+ argvalue = self .get (request )
328344 try :
329345 return argvalue [item ]
330346 except TypeError as e :
331347 raise ValueError ("(lazy_value) The parameter value returned by `%r` is not compliant with the number"
332348 " of argnames in parametrization (%s). A %s-tuple-like was expected. "
333349 "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 ,
335351 argvalue , item , e .__class__ , e ))
336352
337353
@@ -402,6 +418,7 @@ def lazy_value(valuegetter, # type: Callable[[], Any]
402418
403419 A `lazy_value` is the same thing than a function-scoped fixture, except that the value getter function is not a
404420 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.
405422
406423 By default the associated id is the name of the `valuegetter` callable, but a specific `id` can be provided
407424 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):
439456 return False
440457
441458
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+ """
444465
445466 try :
446467 _is_lazy = is_lazy (argval )
447468 except : # noqa
448469 return argval
449470 else :
450471 if _is_lazy :
451- return argval .get ()
472+ return argval .get (request )
452473 else :
453474 return argval
0 commit comments