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

1import inspect 

2import warnings 

3from collections import namedtuple 

4from collections.abc import MutableMapping 

5from typing import Set 

6 

7import attr 

8 

9from ..compat import ascii_escaped 

10from ..compat import ATTRS_EQ_FIELD 

11from ..compat import getfslineno 

12from ..compat import NOTSET 

13from _pytest.outcomes import fail 

14from _pytest.warning_types import PytestUnknownMarkWarning 

15 

16EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" 

17 

18 

19def istestfunc(func): 

20 return ( 

21 hasattr(func, "__call__") 

22 and getattr(func, "__name__", "<lambda>") != "<lambda>" 

23 ) 

24 

25 

26def get_empty_parameterset_mark(config, argnames, func): 

27 from ..nodes import Collector 

28 

29 requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) 

30 if requested_mark in ("", None, "skip"): 

31 mark = MARK_GEN.skip 

32 elif requested_mark == "xfail": 

33 mark = MARK_GEN.xfail(run=False) 

34 elif requested_mark == "fail_at_collect": 

35 f_name = func.__name__ 

36 _, lineno = getfslineno(func) 

37 raise Collector.CollectError( 

38 "Empty parameter set in '%s' at line %d" % (f_name, lineno + 1) 

39 ) 

40 else: 

41 raise LookupError(requested_mark) 

42 fs, lineno = getfslineno(func) 

43 reason = "got empty parameter set %r, function %s at %s:%d" % ( 

44 argnames, 

45 func.__name__, 

46 fs, 

47 lineno, 

48 ) 

49 return mark(reason=reason) 

50 

51 

52class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 

53 @classmethod 

54 def param(cls, *values, marks=(), id=None): 

55 if isinstance(marks, MarkDecorator): 

56 marks = (marks,) 

57 else: 

58 assert isinstance(marks, (tuple, list, set)) 

59 

60 if id is not None: 

61 if not isinstance(id, str): 

62 raise TypeError( 

63 "Expected id to be a string, got {}: {!r}".format(type(id), id) 

64 ) 

65 id = ascii_escaped(id) 

66 return cls(values, marks, id) 

67 

68 @classmethod 

69 def extract_from(cls, parameterset, force_tuple=False): 

70 """ 

71 :param parameterset: 

72 a legacy style parameterset that may or may not be a tuple, 

73 and may or may not be wrapped into a mess of mark objects 

74 

75 :param force_tuple: 

76 enforce tuple wrapping so single argument tuple values 

77 don't get decomposed and break tests 

78 """ 

79 

80 if isinstance(parameterset, cls): 

81 return parameterset 

82 if force_tuple: 

83 return cls.param(parameterset) 

84 else: 

85 return cls(parameterset, marks=[], id=None) 

86 

87 @staticmethod 

88 def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): 

89 if not isinstance(argnames, (tuple, list)): 

90 argnames = [x.strip() for x in argnames.split(",") if x.strip()] 

91 force_tuple = len(argnames) == 1 

92 else: 

93 force_tuple = False 

94 return argnames, force_tuple 

95 

96 @staticmethod 

97 def _parse_parametrize_parameters(argvalues, force_tuple): 

98 return [ 

99 ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues 

100 ] 

101 

102 @classmethod 

103 def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): 

104 argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) 

105 parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) 

106 del argvalues 

107 

108 if parameters: 

109 # check all parameter sets have the correct number of values 

110 for param in parameters: 

111 if len(param.values) != len(argnames): 

112 msg = ( 

113 '{nodeid}: in "parametrize" the number of names ({names_len}):\n' 

114 " {names}\n" 

115 "must be equal to the number of values ({values_len}):\n" 

116 " {values}" 

117 ) 

118 fail( 

119 msg.format( 

120 nodeid=function_definition.nodeid, 

121 values=param.values, 

122 names=argnames, 

123 names_len=len(argnames), 

124 values_len=len(param.values), 

125 ), 

126 pytrace=False, 

127 ) 

128 else: 

129 # empty parameter set (likely computed at runtime): create a single 

130 # parameter set with NOTSET values, with the "empty parameter set" mark applied to it 

131 mark = get_empty_parameterset_mark(config, argnames, func) 

132 parameters.append( 

133 ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None) 

134 ) 

135 return argnames, parameters 

136 

137 

138@attr.s(frozen=True) 

139class Mark: 

140 #: name of the mark 

141 name = attr.ib(type=str) 

142 #: positional arguments of the mark decorator 

143 args = attr.ib() # List[object] 

144 #: keyword arguments of the mark decorator 

145 kwargs = attr.ib() # Dict[str, object] 

146 

147 def combined_with(self, other): 

148 """ 

149 :param other: the mark to combine with 

150 :type other: Mark 

151 :rtype: Mark 

152 

153 combines by appending args and merging the mappings 

154 """ 

155 assert self.name == other.name 

156 return Mark( 

157 self.name, self.args + other.args, dict(self.kwargs, **other.kwargs) 

158 ) 

159 

160 

161@attr.s 

162class MarkDecorator: 

163 """ A decorator for test functions and test classes. When applied 

164 it will create :class:`Mark` objects which are often created like this:: 

165 

166 mark1 = pytest.mark.NAME # simple MarkDecorator 

167 mark2 = pytest.mark.NAME(name1=value) # parametrized MarkDecorator 

168 

169 and can then be applied as decorators to test functions:: 

170 

171 @mark2 

172 def test_function(): 

173 pass 

174 

175 When a MarkDecorator instance is called it does the following: 

176 

177 1. If called with a single class as its only positional argument and no 

178 additional keyword arguments, it attaches itself to the class so it 

179 gets applied automatically to all test cases found in that class. 

180 2. If called with a single function as its only positional argument and 

181 no additional keyword arguments, it attaches a MarkInfo object to the 

182 function, containing all the arguments already stored internally in 

183 the MarkDecorator. 

184 3. When called in any other case, it performs a 'fake construction' call, 

185 i.e. it returns a new MarkDecorator instance with the original 

186 MarkDecorator's content updated with the arguments passed to this 

187 call. 

188 

189 Note: The rules above prevent MarkDecorator objects from storing only a 

190 single function or class reference as their positional argument with no 

191 additional keyword or positional arguments. 

192 

193 """ 

194 

195 mark = attr.ib(validator=attr.validators.instance_of(Mark)) 

196 

197 @property 

198 def name(self): 

199 """alias for mark.name""" 

200 return self.mark.name 

201 

202 @property 

203 def args(self): 

204 """alias for mark.args""" 

205 return self.mark.args 

206 

207 @property 

208 def kwargs(self): 

209 """alias for mark.kwargs""" 

210 return self.mark.kwargs 

211 

212 @property 

213 def markname(self): 

214 return self.name # for backward-compat (2.4.1 had this attr) 

215 

216 def __repr__(self): 

217 return "<MarkDecorator {!r}>".format(self.mark) 

218 

219 def with_args(self, *args, **kwargs): 

220 """ return a MarkDecorator with extra arguments added 

221 

222 unlike call this can be used even if the sole argument is a callable/class 

223 

224 :return: MarkDecorator 

225 """ 

226 

227 mark = Mark(self.name, args, kwargs) 

228 return self.__class__(self.mark.combined_with(mark)) 

229 

230 def __call__(self, *args, **kwargs): 

231 """ if passed a single callable argument: decorate it with mark info. 

232 otherwise add *args/**kwargs in-place to mark information. """ 

233 if args and not kwargs: 

234 func = args[0] 

235 is_class = inspect.isclass(func) 

236 if len(args) == 1 and (istestfunc(func) or is_class): 

237 store_mark(func, self.mark) 

238 return func 

239 return self.with_args(*args, **kwargs) 

240 

241 

242def get_unpacked_marks(obj): 

243 """ 

244 obtain the unpacked marks that are stored on an object 

245 """ 

246 mark_list = getattr(obj, "pytestmark", []) 

247 if not isinstance(mark_list, list): 

248 mark_list = [mark_list] 

249 return normalize_mark_list(mark_list) 

250 

251 

252def normalize_mark_list(mark_list): 

253 """ 

254 normalizes marker decorating helpers to mark objects 

255 

256 :type mark_list: List[Union[Mark, Markdecorator]] 

257 :rtype: List[Mark] 

258 """ 

259 extracted = [ 

260 getattr(mark, "mark", mark) for mark in mark_list 

261 ] # unpack MarkDecorator 

262 for mark in extracted: 

263 if not isinstance(mark, Mark): 

264 raise TypeError("got {!r} instead of Mark".format(mark)) 

265 return [x for x in extracted if isinstance(x, Mark)] 

266 

267 

268def store_mark(obj, mark): 

269 """store a Mark on an object 

270 this is used to implement the Mark declarations/decorators correctly 

271 """ 

272 assert isinstance(mark, Mark), mark 

273 # always reassign name to avoid updating pytestmark 

274 # in a reference that was only borrowed 

275 obj.pytestmark = get_unpacked_marks(obj) + [mark] 

276 

277 

278class MarkGenerator: 

279 """ Factory for :class:`MarkDecorator` objects - exposed as 

280 a ``pytest.mark`` singleton instance. Example:: 

281 

282 import pytest 

283 @pytest.mark.slowtest 

284 def test_function(): 

285 pass 

286 

287 will set a 'slowtest' :class:`MarkInfo` object 

288 on the ``test_function`` object. """ 

289 

290 _config = None 

291 _markers = set() # type: Set[str] 

292 

293 def __getattr__(self, name: str) -> MarkDecorator: 

294 if name[0] == "_": 

295 raise AttributeError("Marker name must NOT start with underscore") 

296 

297 if self._config is not None: 

298 # We store a set of markers as a performance optimisation - if a mark 

299 # name is in the set we definitely know it, but a mark may be known and 

300 # not in the set. We therefore start by updating the set! 

301 if name not in self._markers: 

302 for line in self._config.getini("markers"): 

303 # example lines: "skipif(condition): skip the given test if..." 

304 # or "hypothesis: tests which use Hypothesis", so to get the 

305 # marker name we split on both `:` and `(`. 

306 marker = line.split(":")[0].split("(")[0].strip() 

307 self._markers.add(marker) 

308 

309 # If the name is not in the set of known marks after updating, 

310 # then it really is time to issue a warning or an error. 

311 if name not in self._markers: 

312 if self._config.option.strict_markers: 

313 fail( 

314 "{!r} not found in `markers` configuration option".format(name), 

315 pytrace=False, 

316 ) 

317 

318 # Raise a specific error for common misspellings of "parametrize". 

319 if name in ["parameterize", "parametrise", "parameterise"]: 

320 __tracebackhide__ = True 

321 fail("Unknown '{}' mark, did you mean 'parametrize'?".format(name)) 

322 

323 warnings.warn( 

324 "Unknown pytest.mark.%s - is this a typo? You can register " 

325 "custom marks to avoid this warning - for details, see " 

326 "https://docs.pytest.org/en/latest/mark.html" % name, 

327 PytestUnknownMarkWarning, 

328 ) 

329 

330 return MarkDecorator(Mark(name, (), {})) 

331 

332 

333MARK_GEN = MarkGenerator() 

334 

335 

336class NodeKeywords(MutableMapping): 

337 def __init__(self, node): 

338 self.node = node 

339 self.parent = node.parent 

340 self._markers = {node.name: True} 

341 

342 def __getitem__(self, key): 

343 try: 

344 return self._markers[key] 

345 except KeyError: 

346 if self.parent is None: 

347 raise 

348 return self.parent.keywords[key] 

349 

350 def __setitem__(self, key, value): 

351 self._markers[key] = value 

352 

353 def __delitem__(self, key): 

354 raise ValueError("cannot delete key in keywords dict") 

355 

356 def __iter__(self): 

357 seen = self._seen() 

358 return iter(seen) 

359 

360 def _seen(self): 

361 seen = set(self._markers) 

362 if self.parent is not None: 

363 seen.update(self.parent.keywords) 

364 return seen 

365 

366 def __len__(self): 

367 return len(self._seen()) 

368 

369 def __repr__(self): 

370 return "<NodeKeywords for node {}>".format(self.node) 

371 

372 

373# mypy cannot find this overload, remove when on attrs>=19.2 

374@attr.s(hash=False, **{ATTRS_EQ_FIELD: False}) # type: ignore 

375class NodeMarkers: 

376 """ 

377 internal structure for storing marks belonging to a node 

378 

379 ..warning:: 

380 

381 unstable api 

382 

383 """ 

384 

385 own_markers = attr.ib(default=attr.Factory(list)) 

386 

387 def update(self, add_markers): 

388 """update the own markers 

389 """ 

390 self.own_markers.extend(add_markers) 

391 

392 def find(self, name): 

393 """ 

394 find markers in own nodes or parent nodes 

395 needs a better place 

396 """ 

397 for mark in self.own_markers: 

398 if mark.name == name: 

399 yield mark 

400 

401 def __iter__(self): 

402 return iter(self.own_markers)