Coverage for src/pytest_cases/common_pytest.py: 71%

348 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-09 20:03 +0000

1# Authors: Sylvain MARIE <sylvain.marie@se.com> 

2# + All contributors to <https://github.com/smarie/python-pytest-cases> 

3# 

4# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE> 

5from __future__ import division 

6 

7import inspect 

8import sys 

9import os 

10from importlib import import_module 

11 

12from makefun import add_signature_parameters, wraps 

13 

14try: # python 3.3+ 

15 from inspect import signature, Parameter 

16except ImportError: 

17 from funcsigs import signature, Parameter # noqa 

18 

19from inspect import isgeneratorfunction, isclass 

20 

21try: 

22 from typing import Union, Callable, Any, Optional, Tuple, Type, Iterable, Sized, List # noqa 

23except ImportError: 

24 pass 

25 

26import pytest 

27from _pytest.python import Metafunc 

28 

29from .common_mini_six import string_types 

30from .common_others import get_function_host 

31from .common_pytest_marks import make_marked_parameter_value, get_param_argnames_as_list, \ 

32 get_pytest_parametrize_marks, get_pytest_usefixture_marks, PYTEST3_OR_GREATER, PYTEST6_OR_GREATER, \ 

33 PYTEST38_OR_GREATER, PYTEST34_OR_GREATER, PYTEST33_OR_GREATER, PYTEST32_OR_GREATER, PYTEST71_OR_GREATER, \ 

34 PYTEST8_OR_GREATER, PYTEST84_OR_GREATER 

35from .common_pytest_lazy_values import is_lazy_value, is_lazy 

36 

37 

38# A decorator that will work to create a fixture containing 'yield', whatever the pytest version, and supports hooks 

39if PYTEST3_OR_GREATER: 39 ↛ 50line 39 didn't jump to line 50 because the condition on line 39 was always true

40 def pytest_fixture(hook=None, **kwargs): 

41 def _decorate(f): 

42 # call hook if needed 

43 if hook is not None: 

44 f = hook(f) 

45 

46 # create the fixture 

47 return pytest.fixture(**kwargs)(f) 

48 return _decorate 

49else: 

50 def pytest_fixture(hook=None, name=None, **kwargs): 

51 """Generator-aware pytest.fixture decorator for legacy pytest versions""" 

52 def _decorate(f): 

53 if name is not None: 

54 # 'name' argument is not supported in this old version, use the __name__ trick. 

55 f.__name__ = name 

56 

57 # call hook if needed 

58 if hook is not None: 

59 f = hook(f) 

60 

61 # create the fixture 

62 if isgeneratorfunction(f): 

63 return pytest.yield_fixture(**kwargs)(f) 

64 else: 

65 return pytest.fixture(**kwargs)(f) 

66 return _decorate 

67 

68 

69def pytest_is_running(): 

70 """Return True if the current process is a pytest run 

71 

72 See https://stackoverflow.com/questions/25188119/test-if-code-is-executed-from-within-a-py-test-session 

73 """ 

74 if PYTEST32_OR_GREATER: 

75 return "PYTEST_CURRENT_TEST" in os.environ 

76 else: 

77 import re 

78 return any(re.findall(r'pytest|py.test', sys.argv[0])) 

79 

80 

81def remove_duplicates(lst): 

82 dset = set() 

83 # relies on the fact that dset.add() always returns None. 

84 return [item for item in lst 

85 if item not in dset and not dset.add(item)] 

86 

87 

88if PYTEST84_OR_GREATER: 88 ↛ 100line 88 didn't jump to line 100 because the condition on line 88 was always true

89 def is_fixture(fixture_fun # type: Any 

90 ): 

91 """ 

92 Returns True if the provided function is a fixture 

93 

94 :param fixture_fun: 

95 :return: 

96 """ 

97 from _pytest.fixtures import FixtureFunctionDefinition 

98 return safe_isinstance(fixture_fun, FixtureFunctionDefinition) 

99else: 

100 def is_fixture(fixture_fun # type: Any 

101 ): 

102 """ 

103 Returns True if the provided function is a fixture 

104 

105 :param fixture_fun: 

106 :return: 

107 """ 

108 try: 

109 fixture_fun._pytestfixturefunction # noqa 

110 return True 

111 except AttributeError: 

112 # not a fixture ? 

113 return False 

114 

115 

116if PYTEST84_OR_GREATER: 116 ↛ 140line 116 didn't jump to line 140 because the condition on line 116 was always true

117 def list_all_fixtures_in(cls_or_module, return_names=True, recurse_to_module=False): 

118 """ 

119 Returns a list containing all fixture names (or symbols if `return_names=False`) 

120 in the given class or module. 

121 

122 Note that `recurse_to_module` can be used so that the fixtures in the parent 

123 module of a class are listed too. 

124 

125 :param cls_or_module: 

126 :param return_names: 

127 :param recurse_to_module: 

128 :return: 

129 """ 

130 res = [get_fixture_name(symb) if return_names else symb 

131 for n, symb in inspect.getmembers(cls_or_module, is_fixture)] 

132 

133 if recurse_to_module and not inspect.ismodule(cls_or_module): 133 ↛ 135line 133 didn't jump to line 135 because the condition on line 133 was never true

134 # TODO currently this only works for a single level of nesting, we should use __qualname__ (py3) or .im_class 

135 host = import_module(cls_or_module.__module__) 

136 res += list_all_fixtures_in(host, recurse_to_module=True, return_names=return_names) 

137 

138 return res 

139else: 

140 def list_all_fixtures_in(cls_or_module, return_names=True, recurse_to_module=False): 

141 """ 

142 Returns a list containing all fixture names (or symbols if `return_names=False`) 

143 in the given class or module. 

144 

145 Note that `recurse_to_module` can be used so that the fixtures in the parent 

146 module of a class are listed too. 

147 

148 :param cls_or_module: 

149 :param return_names: 

150 :param recurse_to_module: 

151 :return: 

152 """ 

153 res = [get_fixture_name(symb) if return_names else symb 

154 for n, symb in inspect.getmembers(cls_or_module, lambda f: inspect.isfunction(f) or inspect.ismethod(f)) 

155 if is_fixture(symb)] 

156 

157 if recurse_to_module and not inspect.ismodule(cls_or_module): 

158 # TODO currently this only works for a single level of nesting, we should use __qualname__ (py3) or .im_class 

159 host = import_module(cls_or_module.__module__) 

160 res += list_all_fixtures_in(host, recurse_to_module=True, return_names=return_names) 

161 

162 return res 

163 

164 

165def safe_isclass(obj # type: object 

166 ): 

167 # type: (...) -> bool 

168 """Ignore any exception via isinstance on Python 3.""" 

169 try: 

170 return isclass(obj) 

171 except Exception: # noqa 

172 return False 

173 

174 

175def safe_isinstance(obj, # type: object 

176 cls): 

177 # type: (...) -> bool 

178 """Ignore any exception via isinstance""" 

179 try: 

180 return isinstance(obj, cls) 

181 except Exception: # noqa 

182 return False 

183 

184 

185def assert_is_fixture(fixture_fun # type: Any 

186 ): 

187 """ 

188 Raises a ValueError if the provided fixture function is not a fixture. 

189 

190 :param fixture_fun: 

191 :return: 

192 """ 

193 if not is_fixture(fixture_fun): 

194 raise ValueError("The provided fixture function does not seem to be a fixture: %s. Did you properly decorate " 

195 "it ?" % fixture_fun) 

196 

197 

198if PYTEST84_OR_GREATER: 198 ↛ 224line 198 didn't jump to line 224 because the condition on line 198 was always true

199 def get_fixture_name(fixture_fun # type: Union[str, Callable] 

200 ): 

201 """ 

202 Internal utility to retrieve the fixture name corresponding to the given fixture function. 

203 Indeed there is currently no pytest API to do this. 

204 

205 Note: this function can receive a string, in which case it is directly returned. 

206 

207 :param fixture_fun: 

208 :return: 

209 """ 

210 if isinstance(fixture_fun, string_types): 

211 return fixture_fun 

212 

213 assert_is_fixture(fixture_fun) 

214 

215 if fixture_fun.name is None: 215 ↛ 219line 215 didn't jump to line 219 because the condition on line 215 was never true

216 # As opposed to pytest < 8.4.0, the merge between custom name and function name has already been made, 

217 # this should not happen. 

218 # See https://github.com/nicoddemus/pytest/commit/ecde993e17efb3f34157642a111ba20f476aa80a 

219 raise NotImplementedError 

220 

221 return fixture_fun.name 

222 

223else: 

224 def get_fixture_name(fixture_fun # type: Union[str, Callable] 

225 ): 

226 """ 

227 Internal utility to retrieve the fixture name corresponding to the given fixture function. 

228 Indeed there is currently no pytest API to do this. 

229 

230 Note: this function can receive a string, in which case it is directly returned. 

231 

232 :param fixture_fun: 

233 :return: 

234 """ 

235 if isinstance(fixture_fun, string_types): 

236 return fixture_fun 

237 assert_is_fixture(fixture_fun) 

238 try: # pytest 3 

239 custom_fixture_name = fixture_fun._pytestfixturefunction.name # noqa 

240 except AttributeError: 

241 try: # pytest 2 

242 custom_fixture_name = fixture_fun.func_name # noqa 

243 except AttributeError: 

244 custom_fixture_name = None 

245 

246 if custom_fixture_name is not None: 

247 # there is a custom fixture name 

248 return custom_fixture_name 

249 else: 

250 obj__name = getattr(fixture_fun, '__name__', None) 

251 if obj__name is not None: 

252 # a function, probably 

253 return obj__name 

254 else: 

255 # a callable object probably 

256 return str(fixture_fun) 

257 

258 

259if PYTEST84_OR_GREATER: 259 ↛ 272line 259 didn't jump to line 272 because the condition on line 259 was always true

260 def get_fixture_scope(fixture_fun): 

261 """ 

262 Internal utility to retrieve the fixture scope corresponding to the given fixture function . 

263 Indeed there is currently no pytest API to do this. 

264 

265 :param fixture_fun: 

266 :return: 

267 """ 

268 assert_is_fixture(fixture_fun) 

269 # See https://github.com/nicoddemus/pytest/commit/ecde993e17efb3f34157642a111ba20f476aa80a 

270 return fixture_fun._fixture_function_marker.scope # noqa 

271else: 

272 def get_fixture_scope(fixture_fun): 

273 """ 

274 Internal utility to retrieve the fixture scope corresponding to the given fixture function . 

275 Indeed there is currently no pytest API to do this. 

276 

277 :param fixture_fun: 

278 :return: 

279 """ 

280 assert_is_fixture(fixture_fun) 

281 return fixture_fun._pytestfixturefunction.scope # noqa 

282 # except AttributeError: 

283 # # pytest 2 

284 # return fixture_fun.func_scope 

285 

286 

287# ---------------- working on pytest nodes (e.g. Function) 

288 

289def is_function_node(node): 

290 try: 

291 node.function # noqa 

292 return True 

293 except AttributeError: 

294 return False 

295 

296 

297def get_parametrization_markers(fnode): 

298 """ 

299 Returns the parametrization marks on a pytest Function node. 

300 :param fnode: 

301 :return: 

302 """ 

303 if PYTEST34_OR_GREATER: 303 ↛ 306line 303 didn't jump to line 306 because the condition on line 303 was always true

304 return list(fnode.iter_markers(name="parametrize")) 

305 else: 

306 return list(fnode.parametrize) 

307 

308 

309def get_param_names(fnode): 

310 """ 

311 Returns a list of parameter names for the given pytest Function node. 

312 parameterization marks containing several names are split 

313 

314 :param fnode: 

315 :return: 

316 """ 

317 p_markers = get_parametrization_markers(fnode) 

318 param_names = [] 

319 for paramz_mark in p_markers: 

320 argnames = paramz_mark.args[0] if len(paramz_mark.args) > 0 else paramz_mark.kwargs['argnames'] 

321 param_names += get_param_argnames_as_list(argnames) 

322 return param_names 

323 

324 

325# ---------- test ids utils --------- 

326def combine_ids(paramid_tuples): 

327 """ 

328 Receives a list of tuples containing ids for each parameterset. 

329 Returns the final ids, that are obtained by joining the various param ids by '-' for each test node 

330 

331 :param paramid_tuples: 

332 :return: 

333 """ 

334 # 

335 return ['-'.join(pid for pid in testid) for testid in paramid_tuples] 

336 

337 

338def make_test_ids(global_ids, id_marks, argnames=None, argvalues=None, precomputed_ids=None): 

339 """ 

340 Creates the proper id for each test based on (higher precedence first) 

341 

342 - any specific id mark from a `pytest.param` (`id_marks`) 

343 - the global `ids` argument of pytest parametrize (`global_ids`) 

344 - the name and value of parameters (`argnames`, `argvalues`) or the precomputed ids(`precomputed_ids`) 

345 

346 See also _pytest.python._idvalset method 

347 

348 :param global_ids: 

349 :param id_marks: 

350 :param argnames: 

351 :param argvalues: 

352 :param precomputed_ids: 

353 :return: 

354 """ 

355 if global_ids is not None: 

356 # overridden at global pytest.mark.parametrize level - this takes precedence. 

357 # resolve possibly infinite generators of ids here 

358 p_ids = resolve_ids(global_ids, argvalues, full_resolve=True) 

359 else: 

360 # default: values-based 

361 if precomputed_ids is not None: 361 ↛ 362line 361 didn't jump to line 362 because the condition on line 361 was never true

362 if argnames is not None or argvalues is not None: 

363 raise ValueError("Only one of `precomputed_ids` or argnames/argvalues should be provided.") 

364 p_ids = precomputed_ids 

365 else: 

366 p_ids = make_test_ids_from_param_values(argnames, argvalues) 

367 

368 # Finally, local pytest.param takes precedence over everything else 

369 for i, _id in enumerate(id_marks): 

370 if _id is not None: 

371 p_ids[i] = _id 

372 return p_ids 

373 

374 

375def resolve_ids(ids, # type: Optional[Union[Callable, Iterable[str]]] 

376 argvalues, # type: Sized(Any) 

377 full_resolve=False # type: bool 

378 ): 

379 # type: (...) -> Union[List[str], Callable] 

380 """ 

381 Resolves the `ids` argument of a parametrized fixture. 

382 

383 If `full_resolve` is False (default), iterable ids will be resolved, but not callable ids. This is useful if the 

384 `argvalues` have not yet been cleaned of possible `pytest.param` wrappers. 

385 

386 If `full_resolve` is True, callable ids will be called using the argvalues, so the result is guaranteed to be a 

387 list. 

388 """ 

389 try: 

390 # an explicit list or generator of ids ? 

391 iter(ids) 

392 except TypeError: 

393 # a callable to apply on the values 

394 if full_resolve: 

395 return [ids(v) for v in argvalues] 

396 else: 

397 # return the callable without resolving 

398 return ids 

399 else: 

400 # iterable. 

401 try: 

402 # a sized container ? (list, set, tuple) 

403 nb_ids = len(ids) 

404 # convert to list 

405 ids = list(ids) 

406 except TypeError: 

407 # a generator. Consume it 

408 ids = [id for id, v in zip(ids, argvalues)] 

409 nb_ids = len(ids) 

410 

411 if nb_ids != len(argvalues): 411 ↛ 412line 411 didn't jump to line 412 because the condition on line 411 was never true

412 raise ValueError("Explicit list or generator of `ids` provided has a different length (%s) than the number " 

413 "of argvalues (%s). Ids provided: %r" % (len(ids), len(argvalues), ids)) 

414 return ids 

415 

416 

417def make_test_ids_from_param_values(param_names, 

418 param_values, 

419 ): 

420 """ 

421 Replicates pytest behaviour to generate the ids when there are several parameters in a single `parametrize. 

422 Note that param_values should not contain marks. 

423 

424 :param param_names: 

425 :param param_values: 

426 :return: a list of param ids 

427 """ 

428 if isinstance(param_names, string_types): 428 ↛ 429line 428 didn't jump to line 429 because the condition on line 428 was never true

429 raise TypeError("param_names must be an iterable. Found %r" % param_names) 

430 

431 nb_params = len(param_names) 

432 if nb_params == 0: 432 ↛ 433line 432 didn't jump to line 433 because the condition on line 432 was never true

433 raise ValueError("empty list provided") 

434 elif nb_params == 1: 

435 paramids = [] 

436 for _idx, v in enumerate(param_values): 

437 _id = mini_idvalset(param_names, (v,), _idx) 

438 paramids.append(_id) 

439 else: 

440 paramids = [] 

441 for _idx, vv in enumerate(param_values): 

442 if len(vv) != nb_params: 442 ↛ 443line 442 didn't jump to line 443 because the condition on line 442 was never true

443 raise ValueError("Inconsistent lengths for parameter names and values: '%s' and '%s'" 

444 "" % (param_names, vv)) 

445 _id = mini_idvalset(param_names, vv, _idx) 

446 paramids.append(_id) 

447 return paramids 

448 

449 

450# ---- ParameterSet api --- 

451# def analyze_parameter_set(pmark=None, argnames=None, argvalues=None, ids=None, check_nb=True): 

452# """ 

453# analyzes a parameter set passed either as a pmark or as distinct 

454# (argnames, argvalues, ids) to extract/construct the various ids, marks, and 

455# values 

456# 

457# See also pytest.Metafunc.parametrize method, that calls in particular 

458# pytest.ParameterSet._for_parametrize and _pytest.python._idvalset 

459# 

460# :param pmark: 

461# :param argnames: 

462# :param argvalues: 

463# :param ids: 

464# :param check_nb: a bool indicating if we should raise an error if len(argnames) > 1 and any argvalue has 

465# a different length than len(argnames) 

466# :return: ids, marks, values 

467# """ 

468# if pmark is not None: 

469# if any(a is not None for a in (argnames, argvalues, ids)): 

470# raise ValueError("Either provide a pmark OR the details") 

471# argnames = pmark.param_names 

472# argvalues = pmark.param_values 

473# ids = pmark.param_ids 

474# 

475# # extract all parameters that have a specific configuration (pytest.param()) 

476# custom_pids, p_marks, p_values = extract_parameterset_info(argnames, argvalues, check_nb=check_nb) 

477# 

478# # get the ids by merging/creating the various possibilities 

479# p_ids = make_test_ids(argnames=argnames, argvalues=p_values, global_ids=ids, id_marks=custom_pids) 

480# 

481# return p_ids, p_marks, p_values 

482 

483 

484def extract_parameterset_info(argnames, argvalues, check_nb=True): 

485 """ 

486 

487 :param argnames: the names in this parameterset 

488 :param argvalues: the values in this parameterset 

489 :param check_nb: a bool indicating if we should raise an error if len(argnames) > 1 and any argvalue has 

490 a different length than len(argnames) 

491 :return: 

492 """ 

493 pids = [] 

494 pmarks = [] 

495 pvalues = [] 

496 if isinstance(argnames, string_types): 496 ↛ 497line 496 didn't jump to line 497 because the condition on line 496 was never true

497 raise TypeError("argnames must be an iterable. Found %r" % argnames) 

498 nbnames = len(argnames) 

499 for v in argvalues: 

500 _pid, _pmark, _pvalue = extract_pset_info_single(nbnames, v) 

501 

502 pids.append(_pid) 

503 pmarks.append(_pmark) 

504 pvalues.append(_pvalue) 

505 

506 if check_nb and nbnames > 1 and (len(_pvalue) != nbnames): 506 ↛ 507line 506 didn't jump to line 507 because the condition on line 506 was never true

507 raise ValueError("Inconsistent number of values in pytest parametrize: %s items found while the " 

508 "number of parameters is %s: %s." % (len(_pvalue), nbnames, _pvalue)) 

509 

510 return pids, pmarks, pvalues 

511 

512 

513def extract_pset_info_single(nbnames, argvalue): 

514 """Return id, marks, value""" 

515 if is_marked_parameter_value(argvalue): 

516 # --id 

517 _id = get_marked_parameter_id(argvalue) 

518 # --marks 

519 marks = get_marked_parameter_marks(argvalue) 

520 # --value(a tuple if this is a tuple parameter) 

521 argvalue = get_marked_parameter_values(argvalue, nbargs=nbnames) 

522 return _id, marks, argvalue[0] if nbnames == 1 else argvalue 

523 else: 

524 # normal argvalue 

525 return None, None, argvalue 

526 

527 

528try: # pytest 3.x+ 

529 from _pytest.mark import ParameterSet # noqa 

530 

531 def is_marked_parameter_value(v): 

532 return isinstance(v, ParameterSet) 

533 

534 def get_marked_parameter_marks(v): 

535 return v.marks 

536 

537 def get_marked_parameter_values(v, nbargs): 

538 """This always returns a tuple. nbargs is useful for pytest2 compatibility """ 

539 return v.values 

540 

541 def get_marked_parameter_id(v): 

542 return v.id 

543 

544except ImportError: # pytest 2.x 

545 from _pytest.mark import MarkDecorator 

546 

547 # noinspection PyPep8Naming 

548 def ParameterSet(values, 

549 id, # noqa 

550 marks): 

551 """ Dummy function (not a class) used only by `parametrize` """ 

552 if id is not None: 

553 raise ValueError("This should not happen as `pytest.param` does not exist in pytest 2") 

554 

555 # smart unpack is required for compatibility 

556 val = values[0] if len(values) == 1 else values 

557 nbmarks = len(marks) 

558 

559 if nbmarks == 0: 

560 return val 

561 elif nbmarks > 1: 

562 raise ValueError("Multiple marks on parameters not supported for old versions of pytest") 

563 else: 

564 # decorate with the MarkDecorator 

565 return marks[0](val) 

566 

567 def is_marked_parameter_value(v): 

568 return isinstance(v, MarkDecorator) 

569 

570 def get_marked_parameter_marks(v): 

571 return [v] 

572 

573 def get_marked_parameter_values(v, nbargs): 

574 """Returns a tuple containing the values""" 

575 

576 # v.args[-1] contains the values. 

577 # see MetaFunc.parametrize in pytest 2 to be convinced :) 

578 

579 # if v.name in ('skip', 'skipif'): 

580 if nbargs == 1: 

581 # the last element of args is not a tuple when there is a single arg. 

582 return (v.args[-1],) 

583 else: 

584 return v.args[-1] 

585 # else: 

586 # raise ValueError("Unsupported mark") 

587 

588 def get_marked_parameter_id(v): 

589 return v.kwargs.get('id', None) 

590 

591 

592def get_pytest_nodeid(metafunc): 

593 try: 

594 return metafunc.definition.nodeid 

595 except AttributeError: 

596 return "unknown" 

597 

598 

599try: 

600 # pytest 7+ : scopes is an enum 

601 from _pytest.scope import Scope 

602 

603 def get_pytest_function_scopeval(): 

604 return Scope.Function 

605 

606 def has_function_scope(fixdef): 

607 return fixdef._scope is Scope.Function 

608 

609 def set_callspec_arg_scope_to_function(callspec, arg_name): 

610 callspec._arg2scope[arg_name] = Scope.Function 

611 

612except ImportError: 

613 try: 

614 # pytest 3+ 

615 from _pytest.fixtures import scopes as pt_scopes 

616 except ImportError: 

617 # pytest 2 

618 from _pytest.python import scopes as pt_scopes 

619 

620 # def get_pytest_scopenum(scope_str): 

621 # return pt_scopes.index(scope_str) 

622 

623 def get_pytest_function_scopeval(): 

624 return pt_scopes.index("function") 

625 

626 def has_function_scope(fixdef): 

627 return fixdef.scopenum == get_pytest_function_scopeval() 

628 

629 def set_callspec_arg_scope_to_function(callspec, arg_name): 

630 callspec._arg2scopenum[arg_name] = get_pytest_function_scopeval() # noqa 

631 

632 

633def in_callspec_explicit_args( 

634 callspec, # type: CallSpec2 

635 name # type: str 

636): # type: (...) -> bool 

637 """Return True if name is explicitly used in callspec args""" 

638 return (name in callspec.params) or (not PYTEST8_OR_GREATER and name in callspec.funcargs) 

639 

640 

641if PYTEST71_OR_GREATER: 641 ↛ 647line 641 didn't jump to line 647 because the condition on line 641 was always true

642 from _pytest.python import IdMaker # noqa 

643 

644 _idval = IdMaker([], [], None, None, None, None, None)._idval 

645 _idval_kwargs = dict() 

646else: 

647 from _pytest.python import _idval # noqa 

648 

649 if PYTEST6_OR_GREATER: 

650 _idval_kwargs = dict(idfn=None, 

651 nodeid=None, # item is not used in pytest(>=6.0.0) nodeid is only used by idfn 

652 config=None # if a config hook was available it would be used before this is called) 

653 ) 

654 elif PYTEST38_OR_GREATER: 

655 _idval_kwargs = dict(idfn=None, 

656 item=None, # item is only used by idfn 

657 config=None # if a config hook was available it would be used before this is called) 

658 ) 

659 else: 

660 _idval_kwargs = dict(idfn=None, 

661 # config=None # if a config hook was available it would be used before this is called) 

662 ) 

663 

664 

665def mini_idval( 

666 val, # type: object 

667 argname, # type: str 

668 idx, # type: int 

669): 

670 """ 

671 A simplified version of idval where idfn, item and config do not need to be passed. 

672 

673 :param val: 

674 :param argname: 

675 :param idx: 

676 :return: 

677 """ 

678 return _idval(val=val, argname=argname, idx=idx, **_idval_kwargs) 

679 

680 

681def mini_idvalset(argnames, argvalues, idx): 

682 """ mimic _pytest.python._idvalset but can handle lazyvalues used for tuples or args 

683 

684 argvalues should not be a pytest.param (ParameterSet) 

685 This function returns a SINGLE id for a single test node 

686 """ 

687 if len(argnames) > 1 and is_lazy(argvalues): 

688 # handle the case of LazyTuple used for several args 

689 return argvalues.get_id() 

690 

691 this_id = [ 

692 _idval(val, argname, idx=idx, **_idval_kwargs) 

693 for val, argname in zip(argvalues, argnames) 

694 ] 

695 return "-".join(this_id) 

696 

697 

698try: 

699 from _pytest.compat import getfuncargnames # noqa 

700except ImportError: 

701 def num_mock_patch_args(function): 

702 """ return number of arguments used up by mock arguments (if any) """ 

703 patchings = getattr(function, "patchings", None) 

704 if not patchings: 

705 return 0 

706 

707 mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object()) 

708 ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object()) 

709 

710 return len( 

711 [p for p in patchings if not p.attribute_name and (p.new is mock_sentinel or p.new is ut_mock_sentinel)] 

712 ) 

713 

714 # noinspection SpellCheckingInspection 

715 def getfuncargnames(function, cls=None): 

716 """Returns the names of a function's mandatory arguments.""" 

717 parameters = signature(function).parameters 

718 

719 arg_names = tuple( 

720 p.name 

721 for p in parameters.values() 

722 if ( 

723 p.kind is Parameter.POSITIONAL_OR_KEYWORD 

724 or p.kind is Parameter.KEYWORD_ONLY 

725 ) 

726 and p.default is Parameter.empty 

727 ) 

728 

729 # If this function should be treated as a bound method even though 

730 # it's passed as an unbound method or function, remove the first 

731 # parameter name. 

732 if cls and not isinstance(cls.__dict__.get(function.__name__, None), staticmethod): 

733 arg_names = arg_names[1:] 

734 # Remove any names that will be replaced with mocks. 

735 if hasattr(function, "__wrapped__"): 

736 arg_names = arg_names[num_mock_patch_args(function):] 

737 return arg_names 

738 

739 

740class FakeSession(object): 

741 __slots__ = ('_fixturemanager',) 

742 

743 def __init__(self): 

744 self._fixturemanager = None 

745 

746 

747class MiniFuncDef(object): 

748 __slots__ = ('nodeid', 'session') 

749 

750 def __init__(self, nodeid): 

751 self.nodeid = nodeid 

752 if PYTEST8_OR_GREATER: 752 ↛ exitline 752 didn't return from function '__init__' because the condition on line 752 was always true

753 self.session = FakeSession() 

754 

755 

756class MiniMetafunc(Metafunc): 

757 """ 

758 A class to know what pytest *would* do for a given function in terms of callspec. 

759 It is ONLY used in function `case_to_argvalues` and only the following are read: 

760 

761 - is_parametrized (bool) 

762 - requires_fixtures (bool) 

763 - fixturenames_not_in_sig (declared used fixtures with @pytest.mark.usefixtures) 

764 

765 Computation of the latter requires 

766 

767 """ 

768 # noinspection PyMissingConstructor 

769 def __init__(self, func): 

770 from .plugin import PYTEST_CONFIG # late import to ensure config has been loaded by now 

771 

772 self.config = PYTEST_CONFIG 

773 

774 # self.config can be `None` if the same module is reloaded by another thread/process inside a test (parallelism) 

775 # In that case, a priori we are outside the pytest main runner so we can silently ignore, this 

776 # MetaFunc will not be used/read by anyone. 

777 # See https://github.com/smarie/python-pytest-cases/issues/242 

778 # 

779 # if self.config is None: 

780 # if pytest_is_running(): 

781 # raise ValueError("Internal error - config has not been correctly loaded. Please report") 

782 

783 self.function = func 

784 self.definition = MiniFuncDef(func.__name__) 

785 self._calls = [] 

786 self._params_directness = {} 

787 # non-default parameters 

788 self.fixturenames = getfuncargnames(func) 

789 # add declared used fixtures with @pytest.mark.usefixtures 

790 self.fixturenames_not_in_sig = [f for f in get_pytest_usefixture_marks(func) if f not in self.fixturenames] 

791 if self.fixturenames_not_in_sig: 

792 self.fixturenames = tuple(self.fixturenames_not_in_sig + list(self.fixturenames)) 

793 

794 if PYTEST8_OR_GREATER: 794 ↛ 799line 794 didn't jump to line 799 because the condition on line 794 was always true

795 # dummy 

796 self._arg2fixturedefs = dict() # type: dict[str, Sequence["FixtureDef[Any]"]] 

797 

798 # get parametrization marks 

799 self.pmarks = get_pytest_parametrize_marks(self.function) 

800 if self.is_parametrized: 

801 self.update_callspecs() 

802 # preserve order 

803 ref_names = self._calls[0].params if PYTEST8_OR_GREATER else self._calls[0].funcargs 

804 self.required_fixtures = tuple(f for f in self.fixturenames if f not in ref_names) 

805 else: 

806 self.required_fixtures = self.fixturenames 

807 

808 @property 

809 def is_parametrized(self): 

810 return len(self.pmarks) > 0 

811 

812 @property 

813 def requires_fixtures(self): 

814 return len(self.required_fixtures) > 0 

815 

816 def update_callspecs(self): 

817 """ 

818 

819 :return: 

820 """ 

821 for pmark in self.pmarks: 

822 if len(pmark.param_names) == 1: 

823 if PYTEST3_OR_GREATER: 823 ↛ 826line 823 didn't jump to line 826 because the condition on line 823 was always true

824 argvals = tuple(v if is_marked_parameter_value(v) else (v,) for v in pmark.param_values) 

825 else: 

826 argvals = [] 

827 for v in pmark.param_values: 

828 if is_marked_parameter_value(v): 

829 newmark = MarkDecorator(v.markname, v.args[:-1] + ((v.args[-1],),), v.kwargs) 

830 argvals.append(newmark) 

831 else: 

832 argvals.append((v,)) 

833 argvals = tuple(argvals) 

834 else: 

835 argvals = pmark.param_values 

836 self.parametrize(argnames=pmark.param_names, argvalues=argvals, ids=pmark.param_ids, 

837 # use indirect = False and scope = 'function' to avoid having to implement complex patches 

838 indirect=False, scope='function') 

839 

840 if not PYTEST33_OR_GREATER: 840 ↛ 843line 840 didn't jump to line 843 because the condition on line 840 was never true

841 # fix the CallSpec2 instances so that the marks appear in an attribute "mark" 

842 # noinspection PyProtectedMember 

843 for c in self._calls: 

844 c.marks = list(c.keywords.values()) 

845 

846 

847def add_fixture_params(func, new_names): 

848 """Creates a wrapper of the given function with additional arguments""" 

849 

850 old_sig = signature(func) 

851 

852 # prepend all new parameters if needed 

853 for n in new_names: 

854 if n in old_sig.parameters: 854 ↛ 855line 854 didn't jump to line 855 because the condition on line 854 was never true

855 raise ValueError("argument named %s already present in signature" % n) 

856 new_sig = add_signature_parameters(old_sig, 

857 first=[Parameter(n, kind=Parameter.POSITIONAL_OR_KEYWORD) for n in new_names]) 

858 

859 assert not isgeneratorfunction(func) 

860 

861 # normal function with return statement 

862 @wraps(func, new_sig=new_sig) 

863 def wrapped_func(**kwargs): 

864 for n in new_names: 

865 kwargs.pop(n) 

866 return func(**kwargs) 

867 

868 # else: 

869 # # generator function (with a yield statement) 

870 # @wraps(fixture_func, new_sig=new_sig) 

871 # def wrapped_fixture_func(*args, **kwargs): 

872 # request = kwargs['request'] if func_needs_request else kwargs.pop('request') 

873 # if is_used_request(request): 

874 # for res in fixture_func(*args, **kwargs): 

875 # yield res 

876 # else: 

877 # yield NOT_USED 

878 

879 return wrapped_func 

880 

881 

882def get_callspecs(func): 

883 """ 

884 Returns a list of pytest CallSpec objects corresponding to calls that should be made for this parametrized function. 

885 This mini-helper assumes no complex things (scope='function', indirect=False, no fixtures, no custom configuration) 

886 

887 Note that this function is currently only used in tests. 

888 """ 

889 meta = MiniMetafunc(func) 

890 # meta.update_callspecs() 

891 # noinspection PyProtectedMember 

892 return meta._calls 

893 

894 

895def cart_product_pytest(argnames, argvalues): 

896 """ 

897 - do NOT use `itertools.product` as it fails to handle MarkDecorators 

898 - we also unpack tuples associated with several argnames ("a,b") if needed 

899 - we also propagate marks 

900 

901 :param argnames: 

902 :param argvalues: 

903 :return: 

904 """ 

905 # transform argnames into a list of lists 

906 argnames_lists = [get_param_argnames_as_list(_argnames) if len(_argnames) > 0 else [] for _argnames in argnames] 

907 

908 # make the cartesian product per se 

909 argvalues_prod = _cart_product_pytest(argnames_lists, argvalues) 

910 

911 # flatten the list of argnames 

912 argnames_list = [n for nlist in argnames_lists for n in nlist] 

913 

914 # apply all marks to the arvalues 

915 argvalues_prod = [make_marked_parameter_value(tuple(argvalues), marks=marks) if len(marks) > 0 else tuple(argvalues) 

916 for marks, argvalues in argvalues_prod] 

917 

918 return argnames_list, argvalues_prod 

919 

920 

921def _cart_product_pytest(argnames_lists, argvalues): 

922 result = [] 

923 

924 # first perform the sub cartesian product with entries [1:] 

925 sub_product = _cart_product_pytest(argnames_lists[1:], argvalues[1:]) if len(argvalues) > 1 else None 

926 

927 # then do the final product with entry [0] 

928 for x in argvalues[0]: 

929 # handle x 

930 nb_names = len(argnames_lists[0]) 

931 

932 # (1) extract meta-info 

933 x_id, x_marks, x_value = extract_pset_info_single(nb_names, x) 

934 x_marks_lst = list(x_marks) if x_marks is not None else [] 

935 if x_id is not None: 935 ↛ 936line 935 didn't jump to line 936 because the condition on line 935 was never true

936 raise ValueError("It is not possible to specify a sub-param id when using the new parametrization style. " 

937 "Either use the traditional style or customize all ids at once in `idgen`") 

938 

939 # (2) possibly unpack 

940 if nb_names > 1: 

941 # if lazy value, we have to do something 

942 if is_lazy_value(x_value): 

943 x_value_lst = x_value.as_lazy_items_list(nb_names) 

944 else: 

945 x_value_lst = list(x_value) 

946 else: 

947 x_value_lst = [x_value] 

948 

949 # product 

950 if len(argvalues) > 1: 

951 for m, p in sub_product: 

952 # combine marks and values 

953 result.append((x_marks_lst + m, x_value_lst + p)) 

954 else: 

955 result.append((x_marks_lst, x_value_lst)) 

956 

957 return result 

958 

959 

960def inject_host(apply_decorator): 

961 """ 

962 A decorator for function with signature `apply_decorator(f, host)`, in order to inject 'host', the host of f. 

963 

964 Since it is not entirely feasible to detect the host in python, my first implementation was a bit complex: it was 

965 returning an object with custom implementation of __call__ and __get__ methods, both reacting when pytest collection 

966 happens. 

967 

968 That was very complex. Now we rely on an approximate but good enough alternative with `get_function_host` 

969 

970 :param apply_decorator: 

971 :return: 

972 """ 

973 # class _apply_decorator_with_host_tracking(object): 

974 # def __init__(self, _target): 

975 # # This is called when the decorator is applied on the target. Remember the target and result of paramz 

976 # self._target = _target 

977 # self.__wrapped__ = None 

978 # 

979 # def __get__(self, obj, type_=None): 

980 # """ 

981 # When the decorated test function or fixture sits in a cl 

982 # :param obj: 

983 # :param type_: 

984 # :return: 

985 # """ 

986 # # We now know that the parametrized function/fixture self._target sits in obj (a class or a module) 

987 # # We can therefore apply our parametrization accordingly (we need a reference to this host container in 

988 # # order to store fixtures there) 

989 # if self.__wrapped__ is None: 

990 # self.__wrapped__ = 1 # means 'pending', to protect against infinite recursion 

991 # try: 

992 # self.__wrapped__ = apply_decorator(self._target, obj) 

993 # except Exception as e: 

994 # traceback = sys.exc_info()[2] 

995 # reraise(BaseException, e.args, traceback) 

996 # 

997 # # path, lineno = get_fslocation_from_item(self) 

998 # # warn_explicit( 

999 # # "Error parametrizing function %s : [%s] %s" % (self._target, e.__class__, e), 

1000 # # category=None, 

1001 # # filename=str(path), 

1002 # # lineno=lineno + 1 if lineno is not None else None, 

1003 # # ) 

1004 # # 

1005 # # @wraps(self._target) 

1006 # # def _exc_raiser(*args, **kwargs): 

1007 # # raise e 

1008 # # # remove this metadata otherwise pytest will unpack it 

1009 # # del _exc_raiser.__wrapped__ 

1010 # # self.__wrapped__ = _exc_raiser 

1011 # 

1012 # return self.__wrapped__ 

1013 # 

1014 # def __getattribute__(self, item): 

1015 # if item == '__call__': 

1016 # # direct call means that the parametrized function sits in a module. import it 

1017 # host_module = import_module(self._target.__module__) 

1018 # 

1019 # # next time the __call__ attribute will be set so callable() will work 

1020 # self.__call__ = self.__get__(host_module) 

1021 # return self.__call__ 

1022 # else: 

1023 # return object.__getattribute__(self, item) 

1024 # 

1025 # return _apply_decorator_with_host_tracking 

1026 

1027 def apply(test_or_fixture_func): 

1028 # approximate: always returns the module and not the class :( 

1029 # 

1030 # indeed when this is called, the function exists (and its qualname mentions the host class) but the 

1031 # host class is not yet created in the module, so it is not found by our `get_class_that_defined_method` 

1032 # 

1033 # but still ... this is far less complex to debug than the above attempt and it does not yet have side effects.. 

1034 container = get_function_host(test_or_fixture_func) 

1035 return apply_decorator(test_or_fixture_func, container) 

1036 

1037 return apply 

1038 

1039 

1040def get_pytest_request_and_item(request_or_item): 

1041 """Return the `request` and `item` (node) from whatever is provided""" 

1042 try: 

1043 item = request_or_item.node 

1044 except AttributeError: 

1045 item = request_or_item 

1046 request = item._request 

1047 else: 

1048 request = request_or_item 

1049 

1050 return item, request