1 import collections.abc

2 import inspect

3 import warnings

4 from typing import Any

5 from typing import Callable

6 from typing import Collection

7 from typing import Iterable

8 from typing import Iterator

9 from typing import List

10 from typing import Mapping

11 from typing import MutableMapping

12 from typing import NamedTuple

13 from typing import Optional

14 from typing import overload

15 from typing import Sequence

16 from typing import Set

17 from typing import Tuple

18 from typing import Type

19 from typing import TYPE_CHECKING

20 from typing import TypeVar

21 from typing import Union

22

23 import attr

24

25 from .._code import getfslineno

26 from ..compat import ascii_escaped

27 from ..compat import final

28 from ..compat import NOTSET

29 from ..compat import NotSetType

30 from _pytest.config import Config

31 from _pytest.deprecated import check_ispytest

32 from _pytest.outcomes import fail

33 from _pytest.warning_types import PytestUnknownMarkWarning

34

35 if TYPE_CHECKING:

36 from ..nodes import Node

37

38

39 EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"

40

41

42 def istestfunc(func) -> bool:

43 return callable(func) and getattr(func, "__name__", "<lambda>") != "<lambda>"

44

45

46 def get_empty_parameterset_mark(

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

48) -> "MarkDecorator":

49 from ..nodes import Collector

50

51 fs, lineno = getfslineno(func)

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

53 argnames,

54 func.__name__,

55 fs,

56 lineno,

57 )

58

59 requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)

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

61 mark = MARK_GEN.skip(reason=reason)

62 elif requested_mark == "xfail":

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

64 elif requested_mark == "fail_at_collect":

65 f_name = func.__name__

66 _, lineno = getfslineno(func)

67 raise Collector.CollectError(

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

69 )

70 else:

71 raise LookupError(requested_mark)

72 return mark

73

74

75 class ParameterSet(NamedTuple):

76 values: Sequence[Union[object, NotSetType]]

77 marks: Collection[Union["MarkDecorator", "Mark"]]

78 id: Optional[str]

79

80 @classmethod

81 def param(

82 cls,

83 *values: object,

84 marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),

85 id: Optional[str] = None,

86 ) -> "ParameterSet":

87 if isinstance(marks, MarkDecorator):

88 marks = (marks,)

89 else:

90 assert isinstance(marks, collections.abc.Collection)

91

92 if id is not None:

93 if not isinstance(id, str):

94 raise TypeError(f"Expected id to be a string, got {type(id)}: {id!r}")

95 id = ascii_escaped(id)

96 return cls(values, marks, id)

97

98 @classmethod

99 def extract_from(

100 cls,

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

102 force_tuple: bool = False,

103 ) -> "ParameterSet":

104 """Extract from an object or objects.

105

106 :param parameterset:

107 A legacy style parameterset that may or may not be a tuple,

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

109

110 :param force_tuple:

111 Enforce tuple wrapping so single argument tuple values

112 don't get decomposed and break tests.

113 """

114

115 if isinstance(parameterset, cls):

116 return parameterset

117 if force_tuple:

118 return cls.param(parameterset)

119 else:

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

121 # passes type-checking but crashes:

122 #

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

124 # def test_foo(x, y): pass

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

126

127 @staticmethod

128 def _parse_parametrize_args(

129 argnames: Union[str, Sequence[str]],

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

131 *args,

132 **kwargs,

133 ) -> Tuple[Sequence[str], bool]:

134 if isinstance(argnames, str):

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

136 force_tuple = len(argnames) == 1

137 else:

138 force_tuple = False

139 return argnames, force_tuple

140

141 @staticmethod

142 def _parse_parametrize_parameters(

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

144 force_tuple: bool,

145 ) -> List["ParameterSet"]:

146 return [

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

148 ]

149

150 @classmethod

151 def _for_parametrize(

152 cls,

153 argnames: Union[str, Sequence[str]],

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

155 func,

156 config: Config,

157 nodeid: str,

158 ) -> Tuple[Sequence[str], List["ParameterSet"]]:

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

160 parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)

161 del argvalues

162

163 if parameters:

164 # Check all parameter sets have the correct number of values.

165 for param in parameters:

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

167 msg = (

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

169 " {names}\n"

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

171 " {values}"

172 )

173 fail(

174 msg.format(

175 nodeid=nodeid,

176 values=param.values,

177 names=argnames,

178 names_len=len(argnames),

179 values_len=len(param.values),

180 ),

181 pytrace=False,

182 )

183 else:

184 # Empty parameter set (likely computed at runtime): create a single

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

186 mark = get_empty_parameterset_mark(config, argnames, func)

187 parameters.append(

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

189 )

190 return argnames, parameters

191

192

193@final

194@attr.s(frozen=True, init=False, auto_attribs=True)

195 class Mark:

196 #: Name of the mark.

197 name: str

198 #: Positional arguments of the mark decorator.

199 args: Tuple[Any, ...]

200 #: Keyword arguments of the mark decorator.

201 kwargs: Mapping[str, Any]

202

203 #: Source Mark for ids with parametrize Marks.

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

205 #: Resolved/generated ids with parametrize Marks.

206 _param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)

207

208 def __init__(

209 self,

210 name: str,

211 args: Tuple[Any, ...],

212 kwargs: Mapping[str, Any],

213 param_ids_from: Optional["Mark"] = None,

214 param_ids_generated: Optional[Sequence[str]] = None,

215 *,

216 _ispytest: bool = False,

217 ) -> None:

218 """:meta private:"""

219 check_ispytest(_ispytest)

220 # Weirdness to bypass frozen=True.

221 object.__setattr__(self, "name", name)

222 object.__setattr__(self, "args", args)

223 object.__setattr__(self, "kwargs", kwargs)

224 object.__setattr__(self, "_param_ids_from", param_ids_from)

225 object.__setattr__(self, "_param_ids_generated", param_ids_generated)

226

227 def _has_param_ids(self) -> bool:

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

229

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

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

232 Mark and another Mark.

233

234 Combines by appending args and merging kwargs.

235

236 :param Mark other: The mark to combine with.

237 :rtype: Mark

238 """

239 assert self.name == other.name

240

241 # Remember source of ids with parametrize Marks.

242 param_ids_from: Optional[Mark] = None

243 if self.name == "parametrize":

244 if other._has_param_ids():

245 param_ids_from = other

246 elif self._has_param_ids():

247 param_ids_from = self

248

249 return Mark(

250 self.name,

251 self.args + other.args,

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

253 param_ids_from=param_ids_from,

254 _ispytest=True,

255 )

256

257

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

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

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

261 Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])

262

263

264@attr.s(init=False, auto_attribs=True)

265 class MarkDecorator:

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

267

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

269

270 mark1 = pytest.mark.NAME # Simple MarkDecorator

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

272

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

274

275 @mark2

276 def test_function():

277 pass

278

279 When a ``MarkDecorator`` is called, it does the following:

280

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

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

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

284

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

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

287 containing all the arguments already stored internally in the

288 ``MarkDecorator``.

289

290 3. When called in any other case, it returns a new ``MarkDecorator``

291 instance with the original ``MarkDecorator``'s content updated with

292 the arguments passed to this call.

293

294 Note: The rules above prevent a ``MarkDecorator`` from storing only a

295 single function or class reference as its positional argument with no

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

297 using `with_args()`.

298 """

299

300 mark: Mark

301

302 def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:

303 """:meta private:"""

304 check_ispytest(_ispytest)

305 self.mark = mark

306

307 @property

308 def name(self) -> str:

309 """Alias for mark.name."""

310 return self.mark.name

311

312 @property

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

314 """Alias for mark.args."""

315 return self.mark.args

316

317 @property

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

319 """Alias for mark.kwargs."""

320 return self.mark.kwargs

321

322 @property

323 def markname(self) -> str:

324 """:meta private:"""

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

326

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

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

329

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

331 if the sole argument is a callable/class.

332 """

333 mark = Mark(self.name, args, kwargs, _ispytest=True)

334 return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)

335

336 # Type ignored because the overloads overlap with an incompatible

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

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

339 @overload

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

341 pass

342

343 @overload

344 def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator":

345 pass

346

347 def __call__(self, *args: object, **kwargs: object):

348 """Call the MarkDecorator."""

349 if args and not kwargs:

350 func = args[0]

351 is_class = inspect.isclass(func)

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

353 store_mark(func, self.mark)

354 return func

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

356

357

358 def get_unpacked_marks(

359 obj: Union[object, type],

360 *,

361 consider_mro: bool = True,

362) -> List[Mark]:

363 """Obtain the unpacked marks that are stored on an object.

364

365 If obj is a class and consider_mro is true, return marks applied to

366 this class and all of its super-classes in MRO order. If consider_mro

367 is false, only return marks applied directly to this class.

368 """

369 if isinstance(obj, type):

370 if not consider_mro:

371 mark_lists = [obj.__dict__.get("pytestmark", [])]

372 else:

373 mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]

374 mark_list = []

375 for item in mark_lists:

376 if isinstance(item, list):

377 mark_list.extend(item)

378 else:

379 mark_list.append(item)

380 else:

381 mark_attribute = getattr(obj, "pytestmark", [])

382 if isinstance(mark_attribute, list):

383 mark_list = mark_attribute

384 else:

385 mark_list = [mark_attribute]

386 return list(normalize_mark_list(mark_list))

387

388

389 def normalize_mark_list(

390 mark_list: Iterable[Union[Mark, MarkDecorator]]

391) -> Iterable[Mark]:

392 """

393 Normalize an iterable of Mark or MarkDecorator objects into a list of marks

394 by retrieving the `mark` attribute on MarkDecorator instances.

395

396 :param mark_list: marks to normalize

397 :returns: A new list of the extracted Mark objects

398 """

399 for mark in mark_list:

400 mark_obj = getattr(mark, "mark", mark)

401 if not isinstance(mark_obj, Mark):

402 raise TypeError(f"got {repr(mark_obj)} instead of Mark")

403 yield mark_obj

404

405

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

407 """Store a Mark on an object.

408

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

410 """

411 assert isinstance(mark, Mark), mark

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

413 # was only borrowed.

414 obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]

415

416

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

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

419 if TYPE_CHECKING:

420 from _pytest.scope import _ScopeName

421

422 class _SkipMarkDecorator(MarkDecorator):

423 @overload # type: ignore[override,misc,no-overload-impl]

424 def __call__(self, arg: Markable) -> Markable:

425 ...

426

427 @overload

428 def __call__(self, reason: str = ...) -> "MarkDecorator":

429 ...

430

431 class _SkipifMarkDecorator(MarkDecorator):

432 def __call__( # type: ignore[override]

433 self,

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

435 *conditions: Union[str, bool],

436 reason: str = ...,

437 ) -> MarkDecorator:

438 ...

439

440 class _XfailMarkDecorator(MarkDecorator):

441 @overload # type: ignore[override,misc,no-overload-impl]

442 def __call__(self, arg: Markable) -> Markable:

443 ...

444

445 @overload

446 def __call__(

447 self,

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

449 *conditions: Union[str, bool],

450 reason: str = ...,

451 run: bool = ...,

452 raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,

453 strict: bool = ...,

454 ) -> MarkDecorator:

455 ...

456

457 class _ParametrizeMarkDecorator(MarkDecorator):

458 def __call__( # type: ignore[override]

459 self,

460 argnames: Union[str, Sequence[str]],

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

462 *,

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

464 ids: Optional[

465 Union[

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

467 Callable[[Any], Optional[object]],

468 ]

469 ] = ...,

470 scope: Optional[_ScopeName] = ...,

471 ) -> MarkDecorator:

472 ...

473

474 class _UsefixturesMarkDecorator(MarkDecorator):

475 def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]

476 ...

477

478 class _FilterwarningsMarkDecorator(MarkDecorator):

479 def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override]

480 ...

481

482

483@final

484 class MarkGenerator:

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

486 a ``pytest.mark`` singleton instance.

487

488 Example::

489

490 import pytest

491

492 @pytest.mark.slowtest

493 def test_function():

494 pass

495

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

497 """

498

499 # See TYPE_CHECKING above.

500 if TYPE_CHECKING:

501 skip: _SkipMarkDecorator

502 skipif: _SkipifMarkDecorator

503 xfail: _XfailMarkDecorator

504 parametrize: _ParametrizeMarkDecorator

505 usefixtures: _UsefixturesMarkDecorator

506 filterwarnings: _FilterwarningsMarkDecorator

507

508 def __init__(self, *, _ispytest: bool = False) -> None:

509 check_ispytest(_ispytest)

510 self._config: Optional[Config] = None

511 self._markers: Set[str] = set()

512

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

514 """Generate a new :class:`MarkDecorator` with the given name."""

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

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

517

518 if self._config is not None:

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

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

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

522 if name not in self._markers:

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

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

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

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

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

528 self._markers.add(marker)

529

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

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

532 if name not in self._markers:

533 if self._config.option.strict_markers or self._config.option.strict:

534 fail(

535 f"{name!r} not found in `markers` configuration option",

536 pytrace=False,

537 )

538

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

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

541 __tracebackhide__ = True

542 fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")

543

544 warnings.warn(

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

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

547 "https://docs.pytest.org/en/stable/how-to/mark.html" % name,

548 PytestUnknownMarkWarning,

549 2,

550 )

551

552 return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)

553

554

555 MARK_GEN = MarkGenerator(_ispytest=True)

556

557

558@final

559 class NodeKeywords(MutableMapping[str, Any]):

560 __slots__ = ("node", "parent", "_markers")

561

562 def __init__(self, node: "Node") -> None:

563 self.node = node

564 self.parent = node.parent

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

566

567 def __getitem__(self, key: str) -> Any:

568 try:

569 return self._markers[key]

570 except KeyError:

571 if self.parent is None:

572 raise

573 return self.parent.keywords[key]

574

575 def __setitem__(self, key: str, value: Any) -> None:

576 self._markers[key] = value

577

578 # Note: we could've avoided explicitly implementing some of the methods

579 # below and use the collections.abc fallback, but that would be slow.

580

581 def __contains__(self, key: object) -> bool:

582 return (

583 key in self._markers

584 or self.parent is not None

585 and key in self.parent.keywords

586 )

587

588 def update( # type: ignore[override]

589 self,

590 other: Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] = (),

591 **kwds: Any,

592 ) -> None:

593 self._markers.update(other)

594 self._markers.update(kwds)

595

596 def __delitem__(self, key: str) -> None:

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

598

599 def __iter__(self) -> Iterator[str]:

600 # Doesn't need to be fast.

601 yield from self._markers

602 if self.parent is not None:

603 for keyword in self.parent.keywords:

604 # self._marks and self.parent.keywords can have duplicates.

605 if keyword not in self._markers:

606 yield keyword

607

608 def __len__(self) -> int:

609 # Doesn't need to be fast.

610 return sum(1 for keyword in self)

611

612 def __repr__(self) -> str:

613 return f"<NodeKeywords for node {self.node}>"