Hide keyboard shortcuts

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> 

5 

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. 

10 

11 :param f: 

12 :return: 

13 """ 

14 if callable(f) and hasattr(f, '__name__'): 

15 return f.__name__ 

16 else: 

17 return str(f) 

18 

19 

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 

77 

78 

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. 

82 

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) 

97 

98 return new_id 

99 

100 

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 + ']' 

119 

120 

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 """ 

129 

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 = [] 

137 

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 

141 

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,)) 

144 

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) 

155 

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 

162 

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)) 

188 

189 # Hash 

190 return hash(tuple(l_for_hash)) 

191 

192 

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 

205 

206 

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. 

213 

214 Note: it seems that it is the same than `request.node.funcargs` pytest_node.funcargnames 

215 

216 :param pytest_node: 

217 :return: 

218 """ 

219 return pytest_node.callspec.params 

220 

221 

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`. 

226 

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. 

229 

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 = [] 

237 

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} 

242 

243 return kwargs 

244 

245 

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. 

251 

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 ? 

254 

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) 

265 

266 

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'