Skip to content

Commit 32a6891

Browse files
author
Sylvain MARIE
committed
Fixed issue where the cache of a lazy_value used for a tuple of parameters (several argnames) was not considering the pytest context and thus was wrongly used across pytest nodes. Fixes #202
1 parent 3ad9b39 commit 32a6891

File tree

3 files changed

+88
-23
lines changed

3 files changed

+88
-23
lines changed

pytest_cases/common_pytest_lazy_values.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,8 @@ def copy_from(cls,
147147
"""Creates a copy of this _LazyValue"""
148148
new_obj = cls(valuegetter=obj.valuegetter, id=obj._id, marks=obj._marks)
149149
# make sure the copy will not need to retrieve the result if already done
150-
if obj.has_cached_value():
151-
new_obj.cached_value_context = obj.cached_value_context
152-
new_obj.cached_value = obj.cached_value
150+
new_obj.cached_value_context = obj.cached_value_context
151+
new_obj.cached_value = obj.cached_value
153152
return new_obj
154153

155154
# noinspection PyMissingConstructor
@@ -225,18 +224,44 @@ def get(self, request_or_item):
225224
"""
226225
node = get_test_node(request_or_item)
227226

228-
if self.cached_value_context is None or self.cached_value_context() is not node:
227+
if not self.has_cached_value(node=node):
229228
# retrieve the value by calling the function
230229
self.cached_value = self.valuegetter()
231230
# remember the pytest context of the call with a weak reference to avoir gc issues
232231
self.cached_value_context = weakref.ref(node)
233232

234233
return self.cached_value
235234

236-
def has_cached_value(self):
237-
"""Return True if there is a cached value in self.value, but with no guarantee that it corresponds to the
238-
current request"""
239-
return self.cached_value_context is not None
235+
def has_cached_value(self, request_or_item=None, node=None, raise_if_no_context=True):
236+
"""Return True if there is a cached value in self.value correnponding to the given request
237+
238+
A degraded query "is there a cached value" (whatever the context) can be performed by not passing any
239+
request, item or node, and switching `raise_if_no_context` to False.
240+
241+
:param request_or_item: the pytest request or item
242+
:param node: the pytest node if it already known.
243+
:param raise_if_no_context: a boolean indicating if an error should be raised if `request_or_item` and `node`
244+
are both None. Default is `True`.
245+
"""
246+
if node is None:
247+
# can we get that context information from the request/item ?
248+
if request_or_item is None:
249+
if raise_if_no_context:
250+
raise ValueError("No request, item or node was provided: I can not tell if there is a "
251+
"cached value for your context. Switch `raise_if_no_context=False` if"
252+
" you wish to get a degraded answer.")
253+
else:
254+
# degraded answer: just tell if the cache was populated at least once
255+
return self.cached_value_context is not None
256+
257+
# get node context information
258+
node = get_test_node(request_or_item)
259+
260+
elif request_or_item is not None:
261+
raise ValueError("Only one of `request_or_item` and `node` should be provided")
262+
263+
# True if there is a cached value context that is the same as the context of the request
264+
return self.cached_value_context is not None and self.cached_value_context() is node
240265

241266
def as_lazy_tuple(self, nb_params):
242267
return LazyTuple(self, nb_params)
@@ -280,7 +305,7 @@ def __repr__(self):
280305
"""Override the inherited method to avoid infinite recursion"""
281306
vals_to_display = (
282307
('item', self.item), # item number first for easier debug
283-
('tuple', self.host.cached_value if self.host.has_cached_value() else self.host._lazyvalue), # lazy value tuple or cached tuple
308+
('tuple', self.host.cached_value if self.host.has_cached_value(raise_if_no_context=False) else self.host._lazyvalue), # lazy value tuple or cached tuple
284309
)
285310
return "%s(%s)" % (self.__class__.__name__, ", ".join("%s=%r" % (k, v) for k, v in vals_to_display))
286311

@@ -348,8 +373,19 @@ def get(self, request_or_item):
348373
"""
349374
return self._lazyvalue.get(request_or_item)
350375

351-
def has_cached_value(self):
352-
return self._lazyvalue.has_cached_value()
376+
def has_cached_value(self, request_or_item=None, node=None, raise_if_no_context=True):
377+
"""Return True if there is a cached value correnponding to the given request
378+
379+
A degraded query "is there a cached value" (whatever the context) can be performed by not passing any
380+
request, item or node, and switching `raise_if_no_context` to False.
381+
382+
:param request_or_item: the pytest request or item
383+
:param node: the pytest node if it already known.
384+
:param raise_if_no_context: a boolean indicating if an error should be raised if `request_or_item` and `node`
385+
are both None. Default is `True`.
386+
"""
387+
return self._lazyvalue.has_cached_value(request_or_item=request_or_item, node=node,
388+
raise_if_no_context=raise_if_no_context)
353389

354390
@property
355391
def cached_value(self):
@@ -361,17 +397,16 @@ def __getitem__(self, item):
361397
a facade (a LazyTupleItem), so that pytest can store this item independently wherever needed, without
362398
yet calling the value getter.
363399
"""
364-
if self._lazyvalue.has_cached_value():
365-
# this is never called by pytest, but keep it for debugging
366-
return self._lazyvalue.cached_value[item]
367-
elif item >= self.theoretical_size:
400+
if item >= self.theoretical_size:
368401
raise IndexError(item)
369402
else:
370-
# do not retrieve yet: return a facade
403+
# note: do not use the cache here since we do not know the context.
404+
# return a facade than will be able to use the cache of the tuple
371405
return LazyTupleItem(self, item)
372406

373407
def force_getitem(self, item, request):
374408
""" Call the underlying value getter, then return self[i]. """
409+
# Note: this will use the cache correctly if needed
375410
argvalue = self.get(request)
376411
try:
377412
return argvalue[item]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest_cases
2+
3+
4+
def case_tup():
5+
return [], {} # This can be reproduced with any mutable object
6+
7+
8+
@pytest_cases.fixture(scope="function")
9+
@pytest_cases.parametrize_with_cases("obj1, obj2", cases=".")
10+
def tup(obj1, obj2):
11+
return obj1, obj2
12+
13+
14+
def test_1(tup):
15+
obj1, obj2 = tup
16+
17+
assert len(obj1) == 0
18+
19+
obj1.append(1)
20+
21+
22+
def test_2(tup):
23+
obj1, obj2 = tup
24+
25+
assert len(obj1) == 0
26+
27+
obj1.append(1)

pytest_cases/tests/pytest_extension/parametrize_plus/test_lazy_value_low_level.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,21 @@ def __init__(self):
6262

6363
# now do the same test for lazy values used as a tuple of parameters
6464
new_lv = lazy_value(foo)
65-
assert not new_lv.has_cached_value()
66-
assert a.has_cached_value()
65+
assert not new_lv.has_cached_value(fake_request)
66+
assert a.has_cached_value(fake_request)
67+
with pytest.raises(ValueError):
68+
a.has_cached_value()
6769

6870
for src in new_lv, a:
6971
# set the counter according to the state of the cache
70-
_called = 0 if not src.has_cached_value() else 1
72+
_called = 0 if not src.has_cached_value(fake_request) else 1
7173
at = src.as_lazy_tuple(2)
7274

7375
# test that it is hashable
7476
assert hash(at) == hash((LazyTuple, src, 2))
7577

7678
# test when the tuple is unpacked into several parameters
77-
if not at.has_cached_value():
79+
if not at.has_cached_value(fake_request):
7880
for i, a in enumerate(at):
7981
# test that it is hashable
8082
assert hash(a) == hash((LazyTupleItem, at, i))
@@ -83,8 +85,7 @@ def __init__(self):
8385
assert mini_idval(a, 'a', 1) == 'foo[%s]' % i
8486
assert ('LazyTupleItem(item=%s' % i) in repr(a)
8587
else:
86-
# this does not happen in real usage but in case a plugin messes with this
87-
assert tuple(at) == (1, 2)
88+
assert tuple(at)[0], tuple(at)[1] == (1, 2)
8889

8990
# test when the tuple is not unpacked -
9091
# note: this is not supposed to happen when @parametrize decorates a test function,
@@ -104,7 +105,9 @@ def __init__(self):
104105

105106
# test that retrieving the tuple does not loose the id
106107
assert str(at) == 'foo'
107-
assert at.has_cached_value()
108+
assert at.has_cached_value(fake_request2)
109+
assert not at.has_cached_value(fake_request)
110+
assert at.has_cached_value(raise_if_no_context=False)
108111

109112

110113
def test_lv_clone():

0 commit comments

Comments
 (0)