Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/mark/structures.py: 47%

272 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1import collections.abc 

2import inspect 

3import warnings 

4from typing import Any 

5from typing import Callable 

6from typing import Collection 

7from typing import Iterable 

8from typing import Iterator 

9from typing import List 

10from typing import Mapping 

11from typing import MutableMapping 

12from typing import NamedTuple 

13from typing import Optional 

14from typing import overload 

15from typing import Sequence 

16from typing import Set 

17from typing import Tuple 

18from typing import Type 

19from typing import TYPE_CHECKING 

20from typing import TypeVar 

21from typing import Union 

22 

23import attr 

24 

25from .._code import getfslineno 

26from ..compat import ascii_escaped 

27from ..compat import final 

28from ..compat import NOTSET 

29from ..compat import NotSetType 

30from _pytest.config import Config 

31from _pytest.deprecated import check_ispytest 

32from _pytest.outcomes import fail 

33from _pytest.warning_types import PytestUnknownMarkWarning 

34 

35if TYPE_CHECKING: 

36 from ..nodes import Node 

37 

38 

39EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" 

40 

41 

42def istestfunc(func) -> bool: 

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

44 

45 

46def 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 

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

195class 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. 

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

262 

263 

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

265class 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 

358def 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 

389def 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 

406def 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... 

419if 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 

484class 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 

555MARK_GEN = MarkGenerator(_ispytest=True) 

556 

557 

558@final 

559class 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}>"