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> 

5from copy import copy 

6 

7# WARNING do not import pytest-harvest here: it should remain optional 

8from .steps import _get_step_param_names_or_default 

9from .steps_common import remove_param_from_pytest_node_str_id 

10 

11try: # python 3.5+ 

12 from typing import Union, Iterable, Any 

13except ImportError: 

14 pass 

15 

16 

17def handle_steps_in_results_dct(results_dct, 

18 is_flat=False, # type: bool 

19 raise_if_one_test_without_step_id=False, # type: bool 

20 no_step_id='-', # type: str 

21 step_param_names=None, # type: Union[str, Iterable[str]] 

22 keep_orig_id=True, 

23 no_steps_policy='raise' # type str 

24 ): 

25 """ 

26 Improves the synthesis dictionary so that 

27 - the keys are replaced with a tuple (new_test_id, step_id) where new_test_id is a step-independent test id 

28 - the 'step_id' parameter is removed from the contents 

29 

30 `is_flat` should be set to `True` if the dictionary has been flattened by `pytest-harvest`. 

31 

32 The step id is identified by looking at the pytest parameters, and finding one with a name included in the 

33 `step_param_names` list (`None` uses the default names). If no step id is found on an entry, it is replaced with 

34 the value of `no_step_id` except if `raise_if_one_test_without_step_id=True` - in which case an error is raised. 

35 

36 If all step ids are missing, for all entries in the dictionary, `no_steps_policy` determines what happens: it can 

37 either skip the whole function and return a copy of the input ('skip', or behave as usual ('ignore'), or raise an 

38 error ('raise'). 

39 

40 If `keep_orig_id` is set to True (default), the original id is added to each entry. 

41 

42 :param results_dct: a synthesis dictionary created by `pytest-harvest`. 

43 :param is_flat: to declare that synth_dct was flatten or not (if it was generated using `get_session_synthesis_dct` 

44 with `flatten=True` or `False`). 

45 :param raise_if_one_test_without_step_id: if this is set to `True` and at least one step id can not be found in the 

46 tests, an error will be raised. By default this is set to `False`: in that case, when the step id is not found 

47 it is replaced with value of the `no_step_id` parameter. 

48 :param no_step_id: the identifier to use when the step id is not found (if `raise_if_no_step_id` is `False`) 

49 :param step_param_names: a singleton or iterable containing the names of the test step parameters used in the 

50 tests. By default the list is `[GENERATOR_MODE_STEP_ARGNAME, TEST_STEP_ARGNAME_DEFAULT]` to cover both 

51 generator-mode and legacy manual mode. 

52 :param keep_orig_id: if True (default) the original test id will appear in the dict entries under 'pytest_id' 

53 :param no_steps_policy: if `'ignore` the returned dictionary keys will be tuples (test id, step id) in all 

54 cases, even if no step is present. If 'skip' and no step is present, the method will return a copy of the input 

55 and will not modify anything. If 'raise' (default) and no step is present, an error is raised. 

56 :return: a dictionary where the keys are tuples of (new_test_id, step_id), and the values are copies of the initial 

57 dictionarie's ones, except that the step id parameter is not present anymore 

58 """ 

59 

60 # validate parameters 

61 step_param_names = _get_step_param_names_or_default(step_param_names) 

62 if not isinstance(no_steps_policy, str): 

63 # python 2 compatibility: unicode literals 

64 no_steps_policy = str(no_steps_policy) 

65 if no_steps_policy not in {'ignore', 'raise', 'skip'}: 

66 raise ValueError("`no_steps_policy` should be one of {'ignore', 'raise', 'skip'}") 

67 

68 # edge case of empty dict 

69 if len(results_dct) == 0: 

70 return copy(results_dct) 

71 

72 # create an object of the same container type 

73 res_dct = type(results_dct)() 

74 

75 # fill it 

76 one_step_id_was_present = False 

77 for test_id, test_info in results_dct.items(): 

78 # copy the first level (no deepcopy because we do not want to perform copies of entries in the dict) 

79 new_info = copy(test_info) 

80 if not is_flat: 

81 # non-flattened: all parameters should be in a nested dict entry 

82 if "params" in new_info: 

83 where_params_dct = copy(new_info["params"]) 

84 elif "pytest_params" in new_info: 

85 where_params_dct = copy(new_info["pytest_params"]) 

86 else: 

87 raise KeyError("Could not find information related to parameters in provided dict. Maybe it was " 

88 "created with flatten=True? In this case please set flatten=True here too") 

89 

90 # if there is a 'fixtures' entry, replace it with a copy ? 

91 # not needed a priori 

92 # if 'fixtures' in new_info: 

93 # new_info['fixtures'] = copy(new_info['fixtures']) 

94 else: 

95 # flattened: all parameters should be in dedicated entries at the root level 

96 where_params_dct = new_info 

97 

98 step_name_params = set(step_param_names).intersection(set(where_params_dct.keys())) 

99 if len(step_name_params) == 1: 

100 # remember that there was at least one 

101 one_step_id_was_present = True 

102 

103 # use the key to retrieve step id value and remove it from where it was 

104 step_id_key = step_name_params.pop() 

105 step_id = where_params_dct.pop(step_id_key) 

106 

107 elif len(step_name_params) == 0: 

108 if raise_if_one_test_without_step_id: 

109 raise ValueError("The synthesis dictionary provided does not seem to contain step name parameters for " 

110 "test node '%s'" % test_id) 

111 else: 

112 # use the default id for "no step" 

113 step_id = no_step_id 

114 else: 

115 raise ValueError("The synthesis dictionary provided contains several step name parameters for test node " 

116 "'%s': %s" % (test_id, step_name_params)) 

117 

118 # finally create the new id by replacing in the existing id (whatever its position in the parameters order) 

119 new_id = remove_step_from_test_id(test_id, step_id) 

120 

121 # remember the old id 

122 if keep_orig_id: 

123 new_info['pytest_id'] = new_id 

124 # move it to the beginning of the dict 

125 new_info.move_to_end('pytest_id', last=False) 

126 

127 # store the element 

128 res_dct[(new_id, step_id)] = new_info 

129 

130 if not one_step_id_was_present: 

131 if no_steps_policy == 'skip': 

132 # do not return the modified one, and return the initial dictionary (a copy) 

133 return copy(results_dct) 

134 elif no_steps_policy == 'raise': 

135 raise ValueError("No step ids can be found in provided dictionary. You can ignore this error by switching " 

136 "to `no_steps_policy`='ignore'") 

137 else: 

138 # 'ignore": same 

139 return res_dct 

140 

141 return res_dct 

142 

143 

144handle_steps_in_synthesis_dct = handle_steps_in_results_dct 

145"""deprecated alias - to remove""" 

146 

147 

148def remove_step_from_test_id(test_id, step_id): 

149 """ 

150 Returns a new test id where the step parameter is not present anymore. 

151 

152 :param step_id: 

153 :param test_id: 

154 :return: 

155 """ 

156 # from math import isnan 

157 # if isnan(step_id): 

158 # return test_id 

159 return remove_param_from_pytest_node_str_id(test_id, step_id) 

160 

161 

162def get_all_pytest_param_names_except_step_id(session, 

163 filter=None, # type: Any 

164 filter_incomplete=False, # type: bool 

165 step_param_names=None # type: Union[str, Iterable[str]] 

166 ): 

167 """ 

168 Like pytest-harvest's `get_all_pytest_param_names` this function returns the list of all unique parameter names 

169 used in all items in the provided session, with given filter. However "step id" parameters are removed 

170 automatically. 

171 

172 An optional `filter` can be provided, that can be a singleton or iterable of pytest objects (typically test 

173 functions) and/or module names. 

174 

175 If this method is called before the end of the pytest session, some nodes might be incomplete, i.e. they will not 

176 have data for the three stages (setup/call/teardown). By default these nodes are filtered out but you can set 

177 `filter_incomplete=False` to make them appear. They will have a special 'pending' synthesis status. 

178 

179 :param session: a pytest session object. 

180 :param filter: a singleton or iterable of pytest objects on which to filter the returned dict on (the returned 

181 items will only by pytest nodes for which the pytest object is one of the ones provided). One can also use 

182 modules or the special `THIS MODULE` item. 

183 :param filter_incomplete: a boolean indicating if incomplete nodes (without the three stages setup/call/teardown) 

184 should appear in the results (False) or not (True). Note: by default incomplete nodes DO APPEAR (this is 

185 different from get_session_synthesis_dct behaviour) 

186 :param step_param_names: a singleton or iterable containing the names of the test step parameters used in the 

187 tests. By default the list is `[GENERATOR_MODE_STEP_ARGNAME, TEST_STEP_ARGNAME_DEFAULT]` to cover both 

188 generator-mode and legacy manual mode. 

189 :return: 

190 """ 

191 try: 

192 from pytest_harvest import get_all_pytest_param_names 

193 

194 # test_step_param_names 

195 step_param_names = _get_step_param_names_or_default(step_param_names) 

196 

197 return [p for p in get_all_pytest_param_names(session, filter=filter, filter_incomplete=filter_incomplete) 

198 if p not in step_param_names] 

199 

200 except ImportError: 

201 raise ImportError("pytest-harvest>=1.0.0 is required to use " 

202 "`get_all_pytest_param_names_except_step_id`")