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 collections.abc 

2import inspect 

3import typing 

4import warnings 

5from typing import Any 

6from typing import Callable 

7from typing import Iterable 

8from typing import List 

9from typing import Mapping 

10from typing import NamedTuple 

11from typing import Optional 

12from typing import Sequence 

13from typing import Set 

14from typing import Tuple 

15from typing import TypeVar 

16from typing import Union 

17 

18import attr 

19 

20from .._code import getfslineno 

21from ..compat import ascii_escaped 

22from ..compat import NOTSET 

23from ..compat import NotSetType 

24from ..compat import overload 

25from ..compat import TYPE_CHECKING 

26from _pytest.config import Config 

27from _pytest.outcomes import fail 

28from _pytest.warning_types import PytestUnknownMarkWarning 

29 

30if TYPE_CHECKING: 

31 from typing import Type 

32 

33 

34EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" 

35 

36 

37def istestfunc(func) -> bool: 

38 return ( 

39 hasattr(func, "__call__") 

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

41 ) 

42 

43 

44def get_empty_parameterset_mark( 

45 config: Config, argnames: Sequence[str], func 

46) -> "MarkDecorator": 

47 from ..nodes import Collector 

48 

49 fs, lineno = getfslineno(func) 

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

51 argnames, 

52 func.__name__, 

53 fs, 

54 lineno, 

55 ) 

56 

57 requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) 

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

59 mark = MARK_GEN.skip(reason=reason) 

60 elif requested_mark == "xfail": 

61 mark = MARK_GEN.xfail(reason=reason, run=False) 

62 elif requested_mark == "fail_at_collect": 

63 f_name = func.__name__ 

64 _, lineno = getfslineno(func) 

65 raise Collector.CollectError( 

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

67 ) 

68 else: 

69 raise LookupError(requested_mark) 

70 return mark 

71 

72 

73class ParameterSet( 

74 NamedTuple( 

75 "ParameterSet", 

76 [ 

77 ("values", Sequence[Union[object, NotSetType]]), 

78 ("marks", "typing.Collection[Union[MarkDecorator, Mark]]"), 

79 ("id", Optional[str]), 

80 ], 

81 ) 

82): 

83 @classmethod 

84 def param( 

85 cls, 

86 *values: object, 

87 marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (), 

88 id: Optional[str] = None 

89 ) -> "ParameterSet": 

90 if isinstance(marks, MarkDecorator): 

91 marks = (marks,) 

92 else: 

93 # TODO(py36): Change to collections.abc.Collection. 

94 assert isinstance(marks, (collections.abc.Sequence, set)) 

95 

96 if id is not None: 

97 if not isinstance(id, str): 

98 raise TypeError( 

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

100 ) 

101 id = ascii_escaped(id) 

102 return cls(values, marks, id) 

103 

104 @classmethod 

105 def extract_from( 

106 cls, 

107 parameterset: Union["ParameterSet", Sequence[object], object], 

108 force_tuple: bool = False, 

109 ) -> "ParameterSet": 

110 """ 

111 :param parameterset: 

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

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

114 

115 :param force_tuple: 

116 enforce tuple wrapping so single argument tuple values 

117 don't get decomposed and break tests 

118 """ 

119 

120 if isinstance(parameterset, cls): 

121 return parameterset 

122 if force_tuple: 

123 return cls.param(parameterset) 

124 else: 

125 # TODO: Refactor to fix this type-ignore. Currently the following 

126 # type-checks but crashes: 

127 # 

128 # @pytest.mark.parametrize(('x', 'y'), [1, 2]) 

129 # def test_foo(x, y): pass 

130 return cls(parameterset, marks=[], id=None) # type: ignore[arg-type] 

131 

132 @staticmethod 

133 def _parse_parametrize_args( 

134 argnames: Union[str, List[str], Tuple[str, ...]], 

135 argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], 

136 *args, 

137 **kwargs 

138 ) -> Tuple[Union[List[str], Tuple[str, ...]], bool]: 

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

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

141 force_tuple = len(argnames) == 1 

142 else: 

143 force_tuple = False 

144 return argnames, force_tuple 

145 

146 @staticmethod 

147 def _parse_parametrize_parameters( 

148 argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], 

149 force_tuple: bool, 

150 ) -> List["ParameterSet"]: 

151 return [ 

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

153 ] 

154 

155 @classmethod 

156 def _for_parametrize( 

157 cls, 

158 argnames: Union[str, List[str], Tuple[str, ...]], 

159 argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], 

160 func, 

161 config: Config, 

162 nodeid: str, 

163 ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]: 

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

165 parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) 

166 del argvalues 

167 

168 if parameters: 

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

170 for param in parameters: 

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

172 msg = ( 

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

174 " {names}\n" 

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

176 " {values}" 

177 ) 

178 fail( 

179 msg.format( 

180 nodeid=nodeid, 

181 values=param.values, 

182 names=argnames, 

183 names_len=len(argnames), 

184 values_len=len(param.values), 

185 ), 

186 pytrace=False, 

187 ) 

188 else: 

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

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

191 mark = get_empty_parameterset_mark(config, argnames, func) 

192 parameters.append( 

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

194 ) 

195 return argnames, parameters 

196 

197 

198@attr.s(frozen=True) 

199class Mark: 

200 #: Name of the mark. 

201 name = attr.ib(type=str) 

202 #: Positional arguments of the mark decorator. 

203 args = attr.ib(type=Tuple[Any, ...]) 

204 #: Keyword arguments of the mark decorator. 

205 kwargs = attr.ib(type=Mapping[str, Any]) 

206 

207 #: Source Mark for ids with parametrize Marks. 

208 _param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False) 

209 #: Resolved/generated ids with parametrize Marks. 

210 _param_ids_generated = attr.ib( 

211 type=Optional[Sequence[str]], default=None, repr=False 

212 ) 

213 

214 def _has_param_ids(self) -> bool: 

215 return "ids" in self.kwargs or len(self.args) >= 4 

216 

217 def combined_with(self, other: "Mark") -> "Mark": 

218 """Return a new Mark which is a combination of this 

219 Mark and another Mark. 

220 

221 Combines by appending args and merging kwargs. 

222 

223 :param other: The mark to combine with. 

224 :type other: Mark 

225 :rtype: Mark 

226 """ 

227 assert self.name == other.name 

228 

229 # Remember source of ids with parametrize Marks. 

230 param_ids_from = None # type: Optional[Mark] 

231 if self.name == "parametrize": 

232 if other._has_param_ids(): 

233 param_ids_from = other 

234 elif self._has_param_ids(): 

235 param_ids_from = self 

236 

237 return Mark( 

238 self.name, 

239 self.args + other.args, 

240 dict(self.kwargs, **other.kwargs), 

241 param_ids_from=param_ids_from, 

242 ) 

243 

244 

245# A generic parameter designating an object to which a Mark may 

246# be applied -- a test function (callable) or class. 

247# Note: a lambda is not allowed, but this can't be represented. 

248_Markable = TypeVar("_Markable", bound=Union[Callable[..., object], type]) 

249 

250 

251@attr.s 

252class MarkDecorator: 

253 """A decorator for applying a mark on test functions and classes. 

254 

255 MarkDecorators are created with ``pytest.mark``:: 

256 

257 mark1 = pytest.mark.NAME # Simple MarkDecorator 

258 mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator 

259 

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

261 

262 @mark2 

263 def test_function(): 

264 pass 

265 

266 When a MarkDecorator is called it does the following: 

267 

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

269 additional keyword arguments, it attaches the mark to the class so it 

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

271 

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

273 no additional keyword arguments, it attaches the mark to the function, 

274 containing all the arguments already stored internally in the 

275 MarkDecorator. 

276 

277 3. When called in any other case, it returns a new MarkDecorator instance 

278 with the original MarkDecorator's content updated with the arguments 

279 passed to this call. 

280 

281 Note: The rules above prevent MarkDecorators from storing only a single 

282 function or class reference as their positional argument with no 

283 additional keyword or positional arguments. You can work around this by 

284 using `with_args()`. 

285 """ 

286 

287 mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark)) 

288 

289 @property 

290 def name(self) -> str: 

291 """Alias for mark.name.""" 

292 return self.mark.name 

293 

294 @property 

295 def args(self) -> Tuple[Any, ...]: 

296 """Alias for mark.args.""" 

297 return self.mark.args 

298 

299 @property 

300 def kwargs(self) -> Mapping[str, Any]: 

301 """Alias for mark.kwargs.""" 

302 return self.mark.kwargs 

303 

304 @property 

305 def markname(self) -> str: 

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

307 

308 def __repr__(self) -> str: 

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

310 

311 def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": 

312 """Return a MarkDecorator with extra arguments added. 

313 

314 Unlike calling the MarkDecorator, with_args() can be used even 

315 if the sole argument is a callable/class. 

316 

317 :return: MarkDecorator 

318 """ 

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

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

321 

322 # Type ignored because the overloads overlap with an incompatible 

323 # return type. Not much we can do about that. Thankfully mypy picks 

324 # the first match so it works out even if we break the rules. 

325 @overload 

326 def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc] 

327 raise NotImplementedError() 

328 

329 @overload # noqa: F811 

330 def __call__( # noqa: F811 

331 self, *args: object, **kwargs: object 

332 ) -> "MarkDecorator": 

333 raise NotImplementedError() 

334 

335 def __call__(self, *args: object, **kwargs: object): # noqa: F811 

336 """Call the MarkDecorator.""" 

337 if args and not kwargs: 

338 func = args[0] 

339 is_class = inspect.isclass(func) 

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

341 store_mark(func, self.mark) 

342 return func 

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

344 

345 

346def get_unpacked_marks(obj) -> List[Mark]: 

347 """ 

348 obtain the unpacked marks that are stored on an object 

349 """ 

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

351 if not isinstance(mark_list, list): 

352 mark_list = [mark_list] 

353 return normalize_mark_list(mark_list) 

354 

355 

356def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]: 

357 """ 

358 normalizes marker decorating helpers to mark objects 

359 

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

361 :rtype: List[Mark] 

362 """ 

363 extracted = [ 

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

365 ] # unpack MarkDecorator 

366 for mark in extracted: 

367 if not isinstance(mark, Mark): 

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

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

370 

371 

372def store_mark(obj, mark: Mark) -> None: 

373 """Store a Mark on an object. 

374 

375 This is used to implement the Mark declarations/decorators correctly. 

376 """ 

377 assert isinstance(mark, Mark), mark 

378 # Always reassign name to avoid updating pytestmark in a reference that 

379 # was only borrowed. 

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

381 

382 

383# Typing for builtin pytest marks. This is cheating; it gives builtin marks 

384# special privilege, and breaks modularity. But practicality beats purity... 

385if TYPE_CHECKING: 

386 from _pytest.fixtures import _Scope 

387 

388 class _SkipMarkDecorator(MarkDecorator): 

389 @overload # type: ignore[override,misc] 

390 def __call__(self, arg: _Markable) -> _Markable: 

391 raise NotImplementedError() 

392 

393 @overload # noqa: F811 

394 def __call__(self, reason: str = ...) -> "MarkDecorator": # noqa: F811 

395 raise NotImplementedError() 

396 

397 class _SkipifMarkDecorator(MarkDecorator): 

398 def __call__( # type: ignore[override] 

399 self, 

400 condition: Union[str, bool] = ..., 

401 *conditions: Union[str, bool], 

402 reason: str = ... 

403 ) -> MarkDecorator: 

404 raise NotImplementedError() 

405 

406 class _XfailMarkDecorator(MarkDecorator): 

407 @overload # type: ignore[override,misc] 

408 def __call__(self, arg: _Markable) -> _Markable: 

409 raise NotImplementedError() 

410 

411 @overload # noqa: F811 

412 def __call__( # noqa: F811 

413 self, 

414 condition: Union[str, bool] = ..., 

415 *conditions: Union[str, bool], 

416 reason: str = ..., 

417 run: bool = ..., 

418 raises: Union[ 

419 "Type[BaseException]", Tuple["Type[BaseException]", ...] 

420 ] = ..., 

421 strict: bool = ... 

422 ) -> MarkDecorator: 

423 raise NotImplementedError() 

424 

425 class _ParametrizeMarkDecorator(MarkDecorator): 

426 def __call__( # type: ignore[override] 

427 self, 

428 argnames: Union[str, List[str], Tuple[str, ...]], 

429 argvalues: Iterable[Union[ParameterSet, Sequence[object], object]], 

430 *, 

431 indirect: Union[bool, Sequence[str]] = ..., 

432 ids: Optional[ 

433 Union[ 

434 Iterable[Union[None, str, float, int, bool]], 

435 Callable[[Any], Optional[object]], 

436 ] 

437 ] = ..., 

438 scope: Optional[_Scope] = ... 

439 ) -> MarkDecorator: 

440 raise NotImplementedError() 

441 

442 class _UsefixturesMarkDecorator(MarkDecorator): 

443 def __call__( # type: ignore[override] 

444 self, *fixtures: str 

445 ) -> MarkDecorator: 

446 raise NotImplementedError() 

447 

448 class _FilterwarningsMarkDecorator(MarkDecorator): 

449 def __call__( # type: ignore[override] 

450 self, *filters: str 

451 ) -> MarkDecorator: 

452 raise NotImplementedError() 

453 

454 

455class MarkGenerator: 

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

457 a ``pytest.mark`` singleton instance. 

458 

459 Example:: 

460 

461 import pytest 

462 

463 @pytest.mark.slowtest 

464 def test_function(): 

465 pass 

466 

467 applies a 'slowtest' :class:`Mark` on ``test_function``. 

468 """ 

469 

470 _config = None # type: Optional[Config] 

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

472 

473 # See TYPE_CHECKING above. 

474 if TYPE_CHECKING: 

475 # TODO(py36): Change to builtin annotation syntax. 

476 skip = _SkipMarkDecorator(Mark("skip", (), {})) 

477 skipif = _SkipifMarkDecorator(Mark("skipif", (), {})) 

478 xfail = _XfailMarkDecorator(Mark("xfail", (), {})) 

479 parametrize = _ParametrizeMarkDecorator(Mark("parametrize", (), {})) 

480 usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures", (), {})) 

481 filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings", (), {})) 

482 

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

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

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

486 

487 if self._config is not None: 

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

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

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

491 if name not in self._markers: 

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

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

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

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

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

497 self._markers.add(marker) 

498 

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

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

501 if name not in self._markers: 

502 if self._config.option.strict_markers: 

503 fail( 

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

505 pytrace=False, 

506 ) 

507 

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

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

510 __tracebackhide__ = True 

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

512 

513 warnings.warn( 

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

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

516 "https://docs.pytest.org/en/stable/mark.html" % name, 

517 PytestUnknownMarkWarning, 

518 2, 

519 ) 

520 

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

522 

523 

524MARK_GEN = MarkGenerator() 

525 

526 

527class NodeKeywords(collections.abc.MutableMapping): 

528 def __init__(self, node): 

529 self.node = node 

530 self.parent = node.parent 

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

532 

533 def __getitem__(self, key): 

534 try: 

535 return self._markers[key] 

536 except KeyError: 

537 if self.parent is None: 

538 raise 

539 return self.parent.keywords[key] 

540 

541 def __setitem__(self, key, value): 

542 self._markers[key] = value 

543 

544 def __delitem__(self, key): 

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

546 

547 def __iter__(self): 

548 seen = self._seen() 

549 return iter(seen) 

550 

551 def _seen(self): 

552 seen = set(self._markers) 

553 if self.parent is not None: 

554 seen.update(self.parent.keywords) 

555 return seen 

556 

557 def __len__(self) -> int: 

558 return len(self._seen()) 

559 

560 def __repr__(self) -> str: 

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