-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathfixture_cache.py
212 lines (177 loc) · 9.16 KB
/
fixture_cache.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
from collections import OrderedDict
from inspect import isgeneratorfunction
from decopatch import DECORATED, function_decorator
from makefun import wraps, add_signature_parameters
from six import string_types
from pytest_harvest.common import get_scope
try: # python 3+
from typing import Union, Any, Dict, Callable
except ImportError:
pass
try: # python 3.3+
from inspect import signature, Parameter
except ImportError:
from funcsigs import signature, Parameter
@function_decorator
def saved_fixture(store='fixture_store', # type: Union[str, Dict[str, Any]]
key=None, # type: str
views=None, # type: Dict[str, Callable[[Any], Any]]
save_raw=None, # type: bool
fixture_fun=DECORATED
):
"""
Decorates a fixture so that it is saved in `store`. `store` can be a dict-like variable or a string
representing a fixture name to a dict-like session-scoped fixture. By default it uses the global 'fixture_store'
fixture provided by this plugin.
After executing all tests, `<store>` will contain a new item under key `<key>` (default is the name of the fixture).
This item is a dictionary <test_id>: <fixture_value> for each test node where the fixture was setup.
```python
import pytest
from pytest_harvest import saved_fixture
@pytest.fixture
@saved_fixture
def dummy():
return 1
def test_dummy(dummy):
pass
def test_synthesis(fixture_store):
print(fixture_store['dummy'])
```
Note that for session-scoped and module-scoped fixtures, not all test ids will appear in the store - only those
for which the fixture was (re)created.
Users can save additional views created from the fixture instance by applying transforms (callable functions). To
do this, users can provide a dictionary under the `views` argument, containing a `{<key>: <procedure>}` dict-like.
For each entry, `<procedure>` will be applied on the fixture instance, and the result will be stored under `<key>`.
`save_raw` controls whether the fixture instance should still be saved in this case (default: `True` if
`views is None`, `False` otherwise).
:param store: a dict-like object or a fixture name corresponding to a dict-like object. in this dictionary, a new
entry will be added for the fixture. This entry will contain a dictionary <test_id>: <fixture_value> for each
test node. By default fixtures are stored in the `fixture_store``fixture.
:param key: the name associated with the stored fixture in the store. By default this is the fixture name.
:param views: an optional dictionary that can be provided to store views created from the fixture, rather than (or
in addition to, if `save_raw=True`) the fixture itself. The dict should contain a `{<key>: <procedure>}`
dict-like. For each entry, `<procedure>` will be applied on the fixture instance, and the result will be stored
under `<key>`.
:param save_raw: controls whether the fixture instance should be saved. `None` (Default) is an automatic behaviour
meaning "`True` if `views is None`, `False` otherwise".
:return: a fixture that will be stored
"""
# default: if views is None, we save the raw fixture. If user wants to save views, we do not save the raw
if save_raw is None:
save_raw = views is None
# Simply get the function name
fixture_name = getattr(fixture_fun, '__name__', None) or str(fixture_fun)
# Name to use for storage
key = key or fixture_name
# is the store a fixture or an object ?
store_is_a_fixture = isinstance(store, string_types)
# if the store object is already available, we can ensure that it is initialized. Otherwise trust pytest for that
if not store_is_a_fixture:
if key in store.keys():
raise ValueError("Key '%s' already exists in store object. Please make sure that your store object is "
"properly initialized as an empty dict-like object, and/or provide a different custom "
"`name` if two stored fixtures share the same storage key name.")
# Note: we can not init the storage[key] entry here because when storage is a fixture, it does not yet exist.
def _init_and_check(request, store):
"""Performs a few checks and returns the key to use for saving (the node id)"""
# find the current node id depending on the scope
scope = get_scope(request)
if scope == 'function':
nodeid = request.node.nodeid
else:
# session- or module-scope
# raise Exception("The `@saved_fixture` decorator is only applicable to function-scope fixtures. `%s`"
# " seems to have scope='%s'. Consider removing `@saved_fixture` or changing "
# "the scope to 'function'." % (fixture_fun, scope))
nodeid = request._pyfuncitem.nodeid
# Init storage if needed
if save_raw:
# init
if key not in store:
store[key] = OrderedDict()
# Check that the node id is unique
if nodeid in store[key]:
raise KeyError("Internal Error - This fixture '%s' was already "
"stored for test id '%s'" % (key, nodeid))
if views is not None:
for k in views.keys():
if k not in store:
store[k] = OrderedDict()
# Check that the node id is unique
if nodeid in store[k]:
raise KeyError("Internal Error - This fixture view '%s' was already "
"stored for test id '%s'" % (k, nodeid))
return nodeid
# We will expose a new signature with additional arguments
orig_sig = signature(fixture_fun)
func_needs_request = 'request' in orig_sig.parameters
new_args = ((Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD),) if not func_needs_request else ()) \
+ ((Parameter(store, kind=Parameter.POSITIONAL_OR_KEYWORD),) if store_is_a_fixture else ())
new_sig = add_signature_parameters(orig_sig, first=new_args)
# Wrap the fixture in the correct mode (generator or not)
if not isgeneratorfunction(fixture_fun):
@wraps(fixture_fun, new_sig=new_sig)
def stored_fixture_function(*args, **kwargs):
"""Wraps a fixture so as to store it before it is returned"""
# get the actual store object
if store_is_a_fixture:
store_ = kwargs.pop(store) # read and remove it
else:
# use the variable from outer scope (from `make_saved_fixture`)
store_ = store
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
nodeid = _init_and_check(request, store_)
fixture_value = fixture_fun(*args, **kwargs) # Get the fixture
_store_fixture_and_views(store_, nodeid, key, fixture_value, views, save_raw) # Store it
return fixture_value # Return it
else:
@wraps(fixture_fun, new_sig=new_sig)
def stored_fixture_function(*args, **kwargs):
"""Wraps a fixture so as to store it before it is returned (generator mode)"""
# get the actual store object
if store_is_a_fixture:
store_ = kwargs.pop(store)
else:
# use the variable from outer scope (from `make_saved_fixture`)
store_ = store
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
nodeid = _init_and_check(request, store_)
gen = fixture_fun(*args, **kwargs)
fixture_value = next(gen) # Get the fixture
_store_fixture_and_views(store_, nodeid, key, fixture_value, views, save_raw) # Store it
yield fixture_value # Return it
# Make sure to terminate the underlying generator
try:
next(gen)
except StopIteration:
pass
return stored_fixture_function
def _store_fixture_and_views(store_,
node_id,
main_key,
fixture_value,
views,
save_raw):
"""
:param store_:
:param node_id:
:param main_key:
:param fixture_value:
:param views:
:param save_raw:
:return:
"""
fix_val = _get_underlying_fixture(fixture_value)
if save_raw:
# store the fixture value itself
store_[main_key][node_id] = fix_val
if views is not None:
for key, proc in views.items():
# store each view
store_[key][node_id] = proc(fix_val)
def _get_underlying_fixture(f):
try:
from pytest_steps.steps_generator import get_underlying_fixture
return get_underlying_fixture(f)
except ImportError:
return f