Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Authors: Sylvain MARIE <sylvain.marie@se.com>
2# + All contributors to <https://github.com/smarie/python-pytest-steps>
3#
4# License: 3-clause BSD, <https://github.com/smarie/python-pytest-steps/blob/master/LICENSE>
6def create_pytest_param_str_id(f):
7 # type: (...) -> str
8 """
9 Returns an id that can be used as a pytest id, from an object.
11 :param f:
12 :return:
13 """
14 if callable(f) and hasattr(f, '__name__'):
15 return f.__name__
16 else:
17 return str(f)
20# def get_pytest_node_str_id_approximate(pytest_node,
21# remove_params=None # type: List[str]
22# ):
23# # type: (...) -> str
24# """
25# Returns the unique id string associated with current parametrized pytest node, skipping parameters listed in
26# `remove_params`.
27#
28# Note: this only works if the id associated with the listed parameters to remove are obtained from the parameter
29# value using the 'str' function ; and if several parameters have the exact same string id it does not work.
30#
31# :param pytest_node:
32# :param remove_params:
33# :return:
34# """
35# if remove_params is None:
36# remove_params = []
37#
38# if len(remove_params) == 0:
39# return pytest_node.id
40# else:
41# # Unfortunately there seem to be no possibility in the pytest api to eliminate a named parameter from a node
42# # id, because the node ids are generated at collection time and the link between ids and parameter names are
43# # forgotten.
44# #
45# # The best we can do seems to be:
46# # unparametrized_node_id = node.parent.nodeid + '::' + node.function.__name__
47# # current_parametrization_id = node.callspec.id
48# #
49# # The goal would be to replace current_parametrization_id with a new one (w/o the selected params) from the
50# # callspec object. This object (a CallSpec2)
51# # - has a list of ids (in node.callspec._idlist)
52# # - has a dictionary of parameter names and values (in node.callspec.params)
53# # - But unfortunately there is no way to know which parameter name corresponds to which id (no order)
54# #
55# # Note: a good way to explore this part of pytest is to put a breakpoint in _pytest.python.Function init()
56#
57# # So we made the decision to rely only on string parsing, not objects
58# node_id_base, node_id_params = split_pytest_node_str_id(pytest_node.nodeid)
59#
60# # Create a new id from the current one, by "removing" the ids of the selected parameters
61# param_values_dct = get_pytest_node_current_param_values(pytest_node)
62# for p_name in remove_params:
63# if p_name not in param_values_dct:
64# raise ValueError("Parameter %s is not a valid parameter name in node %s"
65# "" % (p_name, pytest_node.nodeid))
66# else:
67# # Strong assumption: assume that the id will be str(param_value) of param_value.__name__
68# param_id = create_pytest_param_str_id(param_values_dct[p_name])
69#
70# if param_id in node_id_params:
71# node_id_params = remove_param_from_pytest_node_str_id(node_id_params, param_id)
72# else:
73# raise ValueError("Parameter value %s (for parameter %s) cannot be found in node %s"
74# "" % (param_id, p_name, pytest_node.nodeid))
75#
76# return node_id_base + node_id_params
79def remove_param_from_pytest_node_str_id(test_id, param_id_str):
80 """
81 Returns a new test id where the step parameter is not present anymore.
83 :param test_id:
84 :param param_id_str:
85 :return:
86 """
87 # from math import isnan
88 # if isnan(step_id):
89 # return test_id
90 new_id = test_id.replace('-' + param_id_str + '-', '-', 1)
91 # only continue if previous replacement was not successful to avoid cases where the step id is identical to
92 # another parameter
93 if len(new_id) == len(test_id): 93 ↛ 95line 93 didn't jump to line 95, because the condition on line 93 was never false
94 new_id = test_id.replace('[' + param_id_str + '-', '[', 1)
95 if len(new_id) == len(test_id): 95 ↛ 98line 95 didn't jump to line 98, because the condition on line 95 was never false
96 new_id = test_id.replace('-' + param_id_str + ']', ']', 1)
98 return new_id
101# def split_pytest_node_str_id(node_id_str # type: str
102# ):
103# # type: (...) -> Tuple[str, str]
104# """
105# Splits a pytest node id string into base and parametrization ids
106#
107# :param node_id_str:
108# :return:
109# """
110# result = re.compile('(?P<base_node_id>.*)\[(?P<parametrization>.*)\]\Z').match(node_id_str)
111# if result is None:
112# raise ValueError("pytest node id does not match pytest pattern - cannot split it")
113#
114# # Unpack
115# unparametrized_node_id = result.group('base_node_id')
116# current_parametrization_id = result.group('parametrization')
117#
118# return unparametrized_node_id, '[' + current_parametrization_id + ']'
121# class HashableDict(dict):
122# def __hash__(self):
123# return hash(tuple(sorted(self.items())))
124#
125#
126def get_pytest_node_hash_id(pytest_node,
127 params_to_ignore=None):
128 """
130 :param pytest_node:
131 :param ignore_params:
132 :return:
133 """
134 # Default value
135 if params_to_ignore is None: 135 ↛ 136line 135 didn't jump to line 136, because the condition on line 135 was never true
136 params_to_ignore = []
138 # No need to remove parameters: the usual id can do the job
139 if len(params_to_ignore) == 0: 139 ↛ 140line 139 didn't jump to line 140, because the condition on line 139 was never true
140 return pytest_node.callspec.id
142 # Method 0: use the string id and replace the params to ignore. Not reliable enough
143 # id_without_steps = get_pytest_node_str_id_approximate(request.node, remove_params=(test_step_argname,))
145 # Method 1: rely on parameter indices to build the id
146 # NOT APPROPRIATE: indices might not be always set (according to pytest comments in source)
147 # AND indices do not represent unique values
148 #
149 # # Retrieve the current indice for all parameters
150 # params_indices_dct = get_pytest_node_current_param_indices(pytest_node)
151 #
152 # # Use the same order to build the list of tuples to hash
153 # tpl = tuple((p, params_indices_dct[p]) for p in get_pytest_node_current_param_values(pytest_node)
154 # if p not in params_to_ignore)
156 # Method 2
157 # create a hashable dictionary from the list of parameters AND fixtures VALUES
158 # params = get_pytest_node_current_param_and_fixture_values(request, params_to_ignore={test_step_argname,
159 # steps_data_holder_name, 'request'})
160 # test_id_without_steps = HashableDict(params)
161 # >> "too much", not necessary
163 # Method 3
164 # Hash a tuple containing the parameter names with a hash of their value
165 params_dct = get_pytest_node_current_param_values(pytest_node)
166 # first include the pytest object (the test function)
167 # -- support for bound methods (typically test methods in a test class): "unbind" them
168 try: # python 3
169 test_fun = pytest_node.obj.__func__
170 except AttributeError:
171 try: # python 2
172 test_fun = pytest_node.obj.im_func
173 except AttributeError:
174 test_fun = pytest_node.obj
175 l_for_hash = [test_fun]
176 for p, v in params_dct.items():
177 if p not in params_to_ignore:
178 try:
179 hash_o_v = hash(v)
180 except TypeError:
181 # not hashable, try at least the following for dictionary
182 try:
183 hash_o_v = hash(repr(sorted(v.items())))
184 except AttributeError:
185 raise TypeError("Unable to hash test parameter '%s'. Hashable parameters are required to use steps "
186 "reliably." % v)
187 l_for_hash.append((p, hash_o_v))
189 # Hash
190 return hash(tuple(l_for_hash))
193# def get_pytest_node_current_param_indices(pytest_node):
194# """
195# Returns a dictionary containing all parameter indices in the parameter matrix.
196# Problem: these indices do not represent unique parameter values !
197#
198# Indeed if you have a parameter with two values 'a' and 'b', and use also a second parameter with values 1 and 2,
199# then both indices will go from 0 to 3... at least in pytest 3.4.2
200#
201# :param pytest_node:
202# :return:
203# """
204# return pytest_node.callspec.indices
207def get_pytest_node_current_param_values(pytest_node):
208 """
209 Returns a dictionary containing all parameters and their values for the given call.
210 Like `get_pytest_node_current_param_and_fixture_values` it contains all direct parameters
211 (@pytest.mark.parametrize), but it contains no fixture - instead it contains the fixture *parameters*, and only
212 for parametrized fixtures.
214 Note: it seems that it is the same than `request.node.funcargs` pytest_node.funcargnames
216 :param pytest_node:
217 :return:
218 """
219 return pytest_node.callspec.params
222def get_pytest_node_current_param_and_fixture_values(request,
223 params_to_ignore=None):
224 """
225 Returns a dictionary containing all fixtures and parameters values available in a given test `request`.
227 As opposed to `get_pytest_node_current_param_values`, this contains fixture VALUES and non-parametrized fixtures,
228 whereas `get_pytest_node_current_param_values` only contains the parameters.
230 :param request:
231 :param params_to_ignore: an iterable of parameter or fixture names to ignore in the returned dictionary
232 :return:
233 """
234 # Default value
235 if params_to_ignore is None:
236 params_to_ignore = []
238 # List the values of all the test function parameters that matter
239 kwargs = {argname: get_fixture_or_param_value(request, argname)
240 for argname in request.funcargnames
241 if argname not in params_to_ignore}
243 return kwargs
246def get_fixture_or_param_value(request, fixture_or_param_name):
247 """
248 Returns the value associated with parameter or fixture named `fixture_name`, in provided request context.
249 This is just an easy way to use `getfixturevalue` or `getfuncargvalue` according to whichever is available in
250 current pytest version.
252 Note: it seems that this is the same than `request.node.callspec.params[fixture_or_param_name]` but maybe it is
253 less 'internal' as an api ?
255 :param request:
256 :param fixture_or_param_name:
257 :return:
258 """
259 try:
260 # Pytest 4+ or latest 3.x (to avoid the deprecated warning)
261 return request.getfixturevalue(fixture_or_param_name)
262 except AttributeError:
263 # Pytest 3-
264 return request.getfuncargvalue(fixture_or_param_name)
267def get_scope(request):
268 """
269 Utility method to return the scope of a pytest request
270 :param request:
271 :return:
272 """
273 if request.node is request.session: 273 ↛ 274line 273 didn't jump to line 274, because the condition on line 273 was never true
274 return 'session'
275 elif hasattr(request.node, 'function'): 275 ↛ 278line 275 didn't jump to line 278, because the condition on line 275 was never false
276 return 'function'
277 else:
278 return 'module'