1- import re
1+ # import re
22
33try : # python 3+
44 from typing import Tuple
55except ImportError :
66 pass
77
88
9- def get_pytest_id (f ):
9+ def create_pytest_param_str_id (f ):
1010 # type: (...) -> str
1111 """
1212 Returns an id that can be used as a pytest id, from an object.
@@ -20,95 +20,239 @@ def get_pytest_id(f):
2020 return str (f )
2121
2222
23- def split_node_id (node_id # type: str
24- ):
25- # type: (...) -> Tuple[str, str]
23+ # def get_pytest_node_str_id_approximate(pytest_node,
24+ # remove_params=None # type: List[str]
25+ # ):
26+ # # type: (...) -> str
27+ # """
28+ # Returns the unique id string associated with current parametrized pytest node, skipping parameters listed in
29+ # `remove_params`.
30+ #
31+ # Note: this only works if the id associated with the listed parameters to remove are obtained from the parameter
32+ # value using the 'str' function ; and if several parameters have the exact same string id it does not work.
33+ #
34+ # :param pytest_node:
35+ # :param remove_params:
36+ # :return:
37+ # """
38+ # if remove_params is None:
39+ # remove_params = []
40+ #
41+ # if len(remove_params) == 0:
42+ # return pytest_node.id
43+ # else:
44+ # # Unfortunately there seem to be no possibility in the pytest api to eliminate a named parameter from a node id,
45+ # # because the node ids are generated at collection time and the link between ids and parameter names are
46+ # # forgotten.
47+ # #
48+ # # The best we can do seems to be:
49+ # # unparametrized_node_id = node.parent.nodeid + '::' + node.function.__name__
50+ # # current_parametrization_id = node.callspec.id
51+ # #
52+ # # The goal would be to replace current_parametrization_id with a new one (w/o the selected params) from the
53+ # # callspec object. This object (a CallSpec2)
54+ # # - has a list of ids (in node.callspec._idlist)
55+ # # - has a dictionary of parameter names and values (in node.callspec.params)
56+ # # - But unfortunately there is no way to know which parameter name corresponds to which id (no order)
57+ # #
58+ # # Note: a good way to explore this part of pytest is to put a breakpoint in _pytest.python.Function init()
59+ #
60+ # # So we made the decision to rely only on string parsing, not objects
61+ # node_id_base, node_id_params = split_pytest_node_str_id(pytest_node.nodeid)
62+ #
63+ # # Create a new id from the current one, by "removing" the ids of the selected parameters
64+ # param_values_dct = get_pytest_node_current_param_values(pytest_node)
65+ # for p_name in remove_params:
66+ # if p_name not in param_values_dct:
67+ # raise ValueError("Parameter %s is not a valid parameter name in node %s"
68+ # "" % (p_name, pytest_node.nodeid))
69+ # else:
70+ # # Strong assumption: assume that the id will be str(param_value) of param_value.__name__
71+ # param_id = create_pytest_param_str_id(param_values_dct[p_name])
72+ #
73+ # if param_id in node_id_params:
74+ # node_id_params = remove_param_from_pytest_node_str_id(node_id_params, param_id)
75+ # else:
76+ # raise ValueError("Parameter value %s (for parameter %s) cannot be found in node %s"
77+ # "" % (param_id, p_name, pytest_node.nodeid))
78+ #
79+ # return node_id_base + node_id_params
80+
81+
82+ def remove_param_from_pytest_node_str_id (test_id , param_id_str ):
2683 """
27- Splits a pytest node id into base and parametrization ids
84+ Returns a new test id where the step parameter is not present anymore.
2885
29- :param node_id:
86+ :param test_id:
87+ :param param_id_str:
3088 :return:
3189 """
32- result = re .compile ('(?P<base_node_id>.*)\[(?P<parametrization>.*)\]\Z' ).match (node_id )
33- if result is None :
34- raise ValueError ("pytest node id does not match pytest pattern - cannot split it" )
90+ # from math import isnan
91+ # if isnan(step_id):
92+ # return test_id
93+ new_id = test_id .replace ('-' + param_id_str + '-' , '-' , 1 )
94+ # only continue if previous replacement was not successful to avoid cases where the step id is identical to
95+ # another parameter
96+ if len (new_id ) == len (test_id ):
97+ new_id = test_id .replace ('[' + param_id_str + '-' , '[' , 1 )
98+ if len (new_id ) == len (test_id ):
99+ new_id = test_id .replace ('-' + param_id_str + ']' , ']' , 1 )
35100
36- # Unpack
37- unparametrized_node_id = result .group ('base_node_id' )
38- current_parametrization_id = result .group ('parametrization' )
101+ return new_id
39102
40- return unparametrized_node_id , current_parametrization_id
41103
104+ # def split_pytest_node_str_id(node_id_str # type: str
105+ # ):
106+ # # type: (...) -> Tuple[str, str]
107+ # """
108+ # Splits a pytest node id string into base and parametrization ids
109+ #
110+ # :param node_id_str:
111+ # :return:
112+ # """
113+ # result = re.compile('(?P<base_node_id>.*)\[(?P<parametrization>.*)\]\Z').match(node_id_str)
114+ # if result is None:
115+ # raise ValueError("pytest node id does not match pytest pattern - cannot split it")
116+ #
117+ # # Unpack
118+ # unparametrized_node_id = result.group('base_node_id')
119+ # current_parametrization_id = result.group('parametrization')
120+ #
121+ # return unparametrized_node_id, '[' + current_parametrization_id + ']'
42122
43- def get_id (pytest_node ,
44- remove_params = None # type: List[str]
45- ):
46- # type: (...) -> str
123+
124+ # class HashableDict(dict):
125+ # def __hash__(self):
126+ # return hash(tuple(sorted(self.items())))
127+ #
128+ #
129+ def get_pytest_node_hash_id (pytest_node ,
130+ params_to_ignore = None ):
131+ """
132+
133+ :param pytest_node:
134+ :param ignore_params:
135+ :return:
136+ """
137+ # Default value
138+ if params_to_ignore is None :
139+ params_to_ignore = []
140+
141+ # No need to remove parameters: the usual id can do the job
142+ if len (params_to_ignore ) == 0 :
143+ return pytest_node .callspec .id
144+
145+ # Method 0: use the string id and replace the params to ignore. Not reliable enough
146+ # id_without_steps = get_pytest_node_str_id_approximate(request.node, remove_params=(test_step_argname,))
147+
148+ # Method 1: rely on parameter indices to build the id
149+ # NOT APPROPRIATE: indices might not be always set (according to pytest comments in source)
150+ # AND indices do not represent unique values
151+ #
152+ # # Retrieve the current indice for all parameters
153+ # params_indices_dct = get_pytest_node_current_param_indices(pytest_node)
154+ #
155+ # # Use the same order to build the list of tuples to hash
156+ # tpl = tuple((p, params_indices_dct[p]) for p in get_pytest_node_current_param_values(pytest_node)
157+ # if p not in params_to_ignore)
158+
159+ # Method 2
160+ # create a hashable dictionary from the list of parameters AND fixtures VALUES
161+ # params = get_pytest_node_current_param_and_fixture_values(request, params_to_ignore={test_step_argname,
162+ # steps_data_holder_name, 'request'})
163+ # test_id_without_steps = HashableDict(params)
164+ # >> "too much", not necessary
165+
166+ # Method 3
167+ # Hash a tuple containing the parameter names with a hash of their value
168+ params_dct = get_pytest_node_current_param_values (pytest_node )
169+ l = []
170+ for p , v in params_dct .items ():
171+ if p not in params_to_ignore :
172+ try :
173+ hash_o_v = hash (v )
174+ except TypeError :
175+ # not hashable, try at least the following for dictionary
176+ try :
177+ hash_o_v = hash (repr (sorted (v .items ())))
178+ except AttributeError :
179+ raise TypeError ("Unable to hash test parameter '%s'. Hashable parameters are required to use steps "
180+ "reliably." % v )
181+ l .append ((p , hash_o_v ))
182+
183+ # Hash
184+ return hash (tuple (l ))
185+
186+
187+ # def get_pytest_node_current_param_indices(pytest_node):
188+ # """
189+ # Returns a dictionary containing all parameter indices in the parameter matrix.
190+ # Problem: these indices do not represent unique parameter values !
191+ #
192+ # Indeed if you have a parameter with two values 'a' and 'b', and use also a second parameter with values 1 and 2,
193+ # then both indices will go from 0 to 3... at least in pytest 3.4.2
194+ #
195+ # :param pytest_node:
196+ # :return:
197+ # """
198+ # return pytest_node.callspec.indices
199+
200+
201+ def get_pytest_node_current_param_values (pytest_node ):
202+ """
203+ Returns a dictionary containing all parameters and their values for the given call.
204+ Like `get_pytest_node_current_param_and_fixture_values` it contains all direct parameters
205+ (@pytest.mark.parametrize), but it contains no fixture - instead it contains the fixture *parameters*, and only
206+ for parametrized fixtures.
207+
208+ Note: it seems that it is the same than `request.node.funcargs` pytest_node.funcargnames
209+
210+ :param pytest_node:
211+ :return:
47212 """
48- Returns the unique id associated with current parametrized pytest node, skipping parameters listed in remove_params
213+ return pytest_node . callspec . params
49214
50- Note: this only works if the id associated with the listed parameters are obtained from the parameter
51- value using the 'str' function.
215+
216+ def get_pytest_node_current_param_and_fixture_values (request ,
217+ params_to_ignore = None ):
218+ """
219+ Returns a dictionary containing all fixtures and parameters values available in a given test `request`.
220+
221+ As opposed to `get_pytest_node_current_param_values`, this contains fixture VALUES and non-parametrized fixtures,
222+ whereas `get_pytest_node_current_param_values` only contains the parameters.
52223
53224 :param request:
54- :param remove_params:
225+ :param params_to_ignore: an iterable of parameter or fixture names to ignore in the returned dictionary
55226 :return:
56227 """
57- if remove_params is None :
58- remove_params = []
228+ # Default value
229+ if params_to_ignore is None :
230+ params_to_ignore = []
59231
60- if len (remove_params ) == 0 :
61- return pytest_node .id
62- else :
63- # Unfortunately there seem to be no possibility in the pytest api to eliminate a named parameter from a node id,
64- # because the node ids are generated at collection time and the link between ids and parameter names are
65- # forgotten.
66- #
67- # The best we can do seems to be:
68- # unparametrized_node_id = node.parent.nodeid + '::' + node.function.__name__
69- # current_parametrization_id = node.callspec.id
70- #
71- # The goal would be to replace current_parametrization_id with a new one (w/o the selected params) from the
72- # callspec object. This object (a CallSpec2)
73- # - has a list of ids (in node.callspec._idlist)
74- # - has a dictionary of parameter names and values (in node.callspec.params)
75- # - But unfortunately there is no way to know which parameter name corresponds to which id (no order)
76- #
77- # Note: a good way to explore this part of pytest is to put a breakpoint in _pytest.python.Function init()
78-
79- # So we made the decision to rely only on string parsing, not objects
80- node_id_base , node_id_params = split_node_id (pytest_node .nodeid )
81-
82- # Create a new id from the current one, by "removing" the ids of the selected parameters
83- for p_name in remove_params :
84- if p_name not in pytest_node .callspec .params :
85- raise ValueError ("Parameter %s is not a valid parameter name in node %s"
86- "" % (p_name , pytest_node .nodeid ))
87- else :
88- # Strong assumption: assume that the id will be str(param_value) of param_value.__name__
89- param_id = get_pytest_id (pytest_node .callspec .params [p_name ])
90- if param_id in node_id_params :
91- node_id_params = node_id_params .replace (param_id , '*' )
92- else :
93- raise ValueError ("Parameter value %s (for parameter %s) cannot be found in node %s"
94- "" % (param_id , p_name , pytest_node .nodeid ))
95-
96- return node_id_base + '[' + node_id_params + ']'
97-
98-
99- def get_fixture_value (request , fixture_name ):
232+ # List the values of all the test function parameters that matter
233+ kwargs = {argname : get_fixture_or_param_value (request , argname )
234+ for argname in request .funcargnames
235+ if argname not in params_to_ignore }
236+
237+ return kwargs
238+
239+
240+ def get_fixture_or_param_value (request , fixture_or_param_name ):
100241 """
101- Returns the value associated with fixture named `fixture_name`, in provided request context.
102- This is just an easy way to use `getfixturevalue` or `getfuncargvalue` according to whichever is availabl in
103- current pytest version
242+ Returns the value associated with parameter or fixture named `fixture_name`, in provided request context.
243+ This is just an easy way to use `getfixturevalue` or `getfuncargvalue` according to whichever is available in
244+ current pytest version.
245+
246+ Note: it seems that this is the same than `request.node.callspec.params[fixture_or_param_name]` but maybe it is
247+ less 'internal' as an api ?
104248
105249 :param request:
106- :param fixture_name :
250+ :param fixture_or_param_name :
107251 :return:
108252 """
109253 try :
110254 # Pytest 4+ or latest 3.x (to avoid the deprecated warning)
111- return request .getfixturevalue (fixture_name )
255+ return request .getfixturevalue (fixture_or_param_name )
112256 except AttributeError :
113257 # Pytest 3-
114- return request .getfuncargvalue (fixture_name )
258+ return request .getfuncargvalue (fixture_or_param_name )
0 commit comments