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

321 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-09-26 21:52 +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 

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 never false

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 

88def is_fixture(fixture_fun # type: Any 

89 ): 

90 """ 

91 Returns True if the provided function is a fixture 

92 

93 :param fixture_fun: 

94 :return: 

95 """ 

96 try: 

97 fixture_fun._pytestfixturefunction # noqa 

98 return True 

99 except AttributeError: 

100 # not a fixture ? 

101 return False 

102 

103 

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

105 """ 

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

107 in the given class or module. 

108 

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

110 module of a class are listed too. 

111 

112 :param cls_or_module: 

113 :param return_names: 

114 :param recurse_to_module: 

115 :return: 

116 """ 

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

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

119 if is_fixture(symb)] 

120 

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

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

123 host = import_module(cls_or_module.__module__) 

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

125 

126 return res 

127 

128 

129def safe_isclass(obj # type: object 

130 ): 

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

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

133 try: 

134 return isclass(obj) 

135 except Exception: # noqa 

136 return False 

137 

138 

139def safe_isinstance(obj, # type: object 

140 cls): 

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

142 """Ignore any exception via isinstance""" 

143 try: 

144 return isinstance(obj, cls) 

145 except Exception: # noqa 

146 return False 

147 

148 

149def assert_is_fixture(fixture_fun # type: Any 

150 ): 

151 """ 

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

153 

154 :param fixture_fun: 

155 :return: 

156 """ 

157 if not is_fixture(fixture_fun): 

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

159 "it ?" % fixture_fun) 

160 

161 

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

163 ): 

164 """ 

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

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

167 

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

169 

170 :param fixture_fun: 

171 :return: 

172 """ 

173 if isinstance(fixture_fun, string_types): 

174 return fixture_fun 

175 assert_is_fixture(fixture_fun) 

176 try: # pytest 3 

177 custom_fixture_name = fixture_fun._pytestfixturefunction.name # noqa 

178 except AttributeError: 

179 try: # pytest 2 

180 custom_fixture_name = fixture_fun.func_name # noqa 

181 except AttributeError: 

182 custom_fixture_name = None 

183 

184 if custom_fixture_name is not None: 

185 # there is a custom fixture name 

186 return custom_fixture_name 

187 else: 

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

189 if obj__name is not None: 189 ↛ 194line 189 didn't jump to line 194, because the condition on line 189 was never false

190 # a function, probably 

191 return obj__name 

192 else: 

193 # a callable object probably 

194 return str(fixture_fun) 

195 

196 

197def get_fixture_scope(fixture_fun): 

198 """ 

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

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

201 

202 :param fixture_fun: 

203 :return: 

204 """ 

205 assert_is_fixture(fixture_fun) 

206 return fixture_fun._pytestfixturefunction.scope # noqa 

207 # except AttributeError: 

208 # # pytest 2 

209 # return fixture_fun.func_scope 

210 

211 

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

213 

214def is_function_node(node): 

215 try: 

216 node.function # noqa 

217 return True 

218 except AttributeError: 

219 return False 

220 

221 

222def get_parametrization_markers(fnode): 

223 """ 

224 Returns the parametrization marks on a pytest Function node. 

225 :param fnode: 

226 :return: 

227 """ 

228 if PYTEST34_OR_GREATER: 228 ↛ 231line 228 didn't jump to line 231, because the condition on line 228 was never false

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

230 else: 

231 return list(fnode.parametrize) 

232 

233 

234def get_param_names(fnode): 

235 """ 

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

237 parameterization marks containing several names are split 

238 

239 :param fnode: 

240 :return: 

241 """ 

242 p_markers = get_parametrization_markers(fnode) 

243 param_names = [] 

244 for paramz_mark in p_markers: 

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

246 param_names += get_param_argnames_as_list(argnames) 

247 return param_names 

248 

249 

250# ---------- test ids utils --------- 

251def combine_ids(paramid_tuples): 

252 """ 

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

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

255 

256 :param paramid_tuples: 

257 :return: 

258 """ 

259 # 

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

261 

262 

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

264 """ 

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

266 

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

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

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

270 

271 See also _pytest.python._idvalset method 

272 

273 :param global_ids: 

274 :param id_marks: 

275 :param argnames: 

276 :param argvalues: 

277 :param precomputed_ids: 

278 :return: 

279 """ 

280 if global_ids is not None: 

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

282 # resolve possibly infinite generators of ids here 

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

284 else: 

285 # default: values-based 

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

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

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

289 p_ids = precomputed_ids 

290 else: 

291 p_ids = make_test_ids_from_param_values(argnames, argvalues) 

292 

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

294 for i, _id in enumerate(id_marks): 

295 if _id is not None: 

296 p_ids[i] = _id 

297 return p_ids 

298 

299 

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

301 argvalues, # type: Sized(Any) 

302 full_resolve=False # type: bool 

303 ): 

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

305 """ 

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

307 

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

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

310 

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

312 list. 

313 """ 

314 try: 

315 # an explicit list or generator of ids ? 

316 iter(ids) 

317 except TypeError: 

318 # a callable to apply on the values 

319 if full_resolve: 

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

321 else: 

322 # return the callable without resolving 

323 return ids 

324 else: 

325 # iterable. 

326 try: 

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

328 nb_ids = len(ids) 

329 # convert to list 

330 ids = list(ids) 

331 except TypeError: 

332 # a generator. Consume it 

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

334 nb_ids = len(ids) 

335 

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

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

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

339 return ids 

340 

341 

342def make_test_ids_from_param_values(param_names, 

343 param_values, 

344 ): 

345 """ 

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

347 Note that param_values should not contain marks. 

348 

349 :param param_names: 

350 :param param_values: 

351 :return: a list of param ids 

352 """ 

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

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

355 

356 nb_params = len(param_names) 

357 if nb_params == 0: 357 ↛ 358line 357 didn't jump to line 358, because the condition on line 357 was never true

358 raise ValueError("empty list provided") 

359 elif nb_params == 1: 

360 paramids = [] 

361 for _idx, v in enumerate(param_values): 

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

363 paramids.append(_id) 

364 else: 

365 paramids = [] 

366 for _idx, vv in enumerate(param_values): 

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

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

369 "" % (param_names, vv)) 

370 _id = mini_idvalset(param_names, vv, _idx) 

371 paramids.append(_id) 

372 return paramids 

373 

374 

375# ---- ParameterSet api --- 

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

377# """ 

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

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

380# values 

381# 

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

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

384# 

385# :param pmark: 

386# :param argnames: 

387# :param argvalues: 

388# :param ids: 

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

390# a different length than len(argnames) 

391# :return: ids, marks, values 

392# """ 

393# if pmark is not None: 

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

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

396# argnames = pmark.param_names 

397# argvalues = pmark.param_values 

398# ids = pmark.param_ids 

399# 

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

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

402# 

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

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

405# 

406# return p_ids, p_marks, p_values 

407 

408 

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

410 """ 

411 

412 :param argnames: the names in this parameterset 

413 :param argvalues: the values in this parameterset 

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

415 a different length than len(argnames) 

416 :return: 

417 """ 

418 pids = [] 

419 pmarks = [] 

420 pvalues = [] 

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

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

423 nbnames = len(argnames) 

424 for v in argvalues: 

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

426 

427 pids.append(_pid) 

428 pmarks.append(_pmark) 

429 pvalues.append(_pvalue) 

430 

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

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

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

434 

435 return pids, pmarks, pvalues 

436 

437 

438def extract_pset_info_single(nbnames, argvalue): 

439 """Return id, marks, value""" 

440 if is_marked_parameter_value(argvalue): 

441 # --id 

442 _id = get_marked_parameter_id(argvalue) 

443 # --marks 

444 marks = get_marked_parameter_marks(argvalue) 

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

446 argvalue = get_marked_parameter_values(argvalue, nbargs=nbnames) 

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

448 else: 

449 # normal argvalue 

450 return None, None, argvalue 

451 

452 

453try: # pytest 3.x+ 

454 from _pytest.mark import ParameterSet # noqa 

455 

456 def is_marked_parameter_value(v): 

457 return isinstance(v, ParameterSet) 

458 

459 def get_marked_parameter_marks(v): 

460 return v.marks 

461 

462 def get_marked_parameter_values(v, nbargs): 

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

464 return v.values 

465 

466 def get_marked_parameter_id(v): 

467 return v.id 

468 

469except ImportError: # pytest 2.x 

470 from _pytest.mark import MarkDecorator 

471 

472 # noinspection PyPep8Naming 

473 def ParameterSet(values, 

474 id, # noqa 

475 marks): 

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

477 if id is not None: 

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

479 

480 # smart unpack is required for compatibility 

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

482 nbmarks = len(marks) 

483 

484 if nbmarks == 0: 

485 return val 

486 elif nbmarks > 1: 

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

488 else: 

489 # decorate with the MarkDecorator 

490 return marks[0](val) 

491 

492 def is_marked_parameter_value(v): 

493 return isinstance(v, MarkDecorator) 

494 

495 def get_marked_parameter_marks(v): 

496 return [v] 

497 

498 def get_marked_parameter_values(v, nbargs): 

499 """Returns a tuple containing the values""" 

500 

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

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

503 

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

505 if nbargs == 1: 

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

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

508 else: 

509 return v.args[-1] 

510 # else: 

511 # raise ValueError("Unsupported mark") 

512 

513 def get_marked_parameter_id(v): 

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

515 

516 

517def get_pytest_nodeid(metafunc): 

518 try: 

519 return metafunc.definition.nodeid 

520 except AttributeError: 

521 return "unknown" 

522 

523 

524try: 

525 # pytest 7+ : scopes is an enum 

526 from _pytest.scope import Scope 

527 

528 def get_pytest_function_scopeval(): 

529 return Scope.Function 

530 

531 def has_function_scope(fixdef): 

532 return fixdef._scope is Scope.Function 

533 

534 def set_callspec_arg_scope_to_function(callspec, arg_name): 

535 callspec._arg2scope[arg_name] = Scope.Function 

536 

537except ImportError: 

538 try: 

539 # pytest 3+ 

540 from _pytest.fixtures import scopes as pt_scopes 

541 except ImportError: 

542 # pytest 2 

543 from _pytest.python import scopes as pt_scopes 

544 

545 # def get_pytest_scopenum(scope_str): 

546 # return pt_scopes.index(scope_str) 

547 

548 def get_pytest_function_scopeval(): 

549 return pt_scopes.index("function") 

550 

551 def has_function_scope(fixdef): 

552 return fixdef.scopenum == get_pytest_function_scopeval() 

553 

554 def set_callspec_arg_scope_to_function(callspec, arg_name): 

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

556 

557 

558def in_callspec_explicit_args( 

559 callspec, # type: CallSpec2 

560 name # type: str 

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

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

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

564 

565 

566if PYTEST71_OR_GREATER: 566 ↛ 572line 566 didn't jump to line 572, because the condition on line 566 was never false

567 from _pytest.python import IdMaker # noqa 

568 

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

570 _idval_kwargs = dict() 

571else: 

572 from _pytest.python import _idval # noqa 

573 

574 if PYTEST6_OR_GREATER: 

575 _idval_kwargs = dict(idfn=None, 

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

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

578 ) 

579 elif PYTEST38_OR_GREATER: 

580 _idval_kwargs = dict(idfn=None, 

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

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

583 ) 

584 else: 

585 _idval_kwargs = dict(idfn=None, 

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

587 ) 

588 

589 

590def mini_idval( 

591 val, # type: object 

592 argname, # type: str 

593 idx, # type: int 

594): 

595 """ 

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

597 

598 :param val: 

599 :param argname: 

600 :param idx: 

601 :return: 

602 """ 

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

604 

605 

606def mini_idvalset(argnames, argvalues, idx): 

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

608 

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

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

611 """ 

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

613 # handle the case of LazyTuple used for several args 

614 return argvalues.get_id() 

615 

616 this_id = [ 

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

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

619 ] 

620 return "-".join(this_id) 

621 

622 

623try: 

624 from _pytest.compat import getfuncargnames # noqa 

625except ImportError: 

626 def num_mock_patch_args(function): 

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

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

629 if not patchings: 

630 return 0 

631 

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

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

634 

635 return len( 

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

637 ) 

638 

639 # noinspection SpellCheckingInspection 

640 def getfuncargnames(function, cls=None): 

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

642 parameters = signature(function).parameters 

643 

644 arg_names = tuple( 

645 p.name 

646 for p in parameters.values() 

647 if ( 

648 p.kind is Parameter.POSITIONAL_OR_KEYWORD 

649 or p.kind is Parameter.KEYWORD_ONLY 

650 ) 

651 and p.default is Parameter.empty 

652 ) 

653 

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

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

656 # parameter name. 

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

658 arg_names = arg_names[1:] 

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

660 if hasattr(function, "__wrapped__"): 

661 arg_names = arg_names[num_mock_patch_args(function):] 

662 return arg_names 

663 

664 

665class FakeSession(object): 

666 __slots__ = ('_fixturemanager',) 

667 

668 def __init__(self): 

669 self._fixturemanager = None 

670 

671 

672class MiniFuncDef(object): 

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

674 

675 def __init__(self, nodeid): 

676 self.nodeid = nodeid 

677 if PYTEST8_OR_GREATER: 677 ↛ 678line 677 didn't jump to line 678, because the condition on line 677 was never true

678 self.session = FakeSession() 

679 

680 

681class MiniMetafunc(Metafunc): 

682 """ 

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

684 It is used in function `case_to_argvalues` 

685 """ 

686 # noinspection PyMissingConstructor 

687 def __init__(self, func): 

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

689 

690 self.config = PYTEST_CONFIG 

691 

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

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

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

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

696 # 

697 # if self.config is None: 

698 # if pytest_is_running(): 

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

700 

701 self.function = func 

702 self.definition = MiniFuncDef(func.__name__) 

703 self._calls = [] 

704 # non-default parameters 

705 self.fixturenames = getfuncargnames(func) 

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

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

708 if self.fixturenames_not_in_sig: 

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

710 

711 if PYTEST8_OR_GREATER: 711 ↛ 713line 711 didn't jump to line 713, because the condition on line 711 was never true

712 # dummy 

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

714 

715 # get parametrization marks 

716 self.pmarks = get_pytest_parametrize_marks(self.function) 

717 if self.is_parametrized: 

718 self.update_callspecs() 

719 # preserve order 

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

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

722 else: 

723 self.required_fixtures = self.fixturenames 

724 

725 @property 

726 def is_parametrized(self): 

727 return len(self.pmarks) > 0 

728 

729 @property 

730 def requires_fixtures(self): 

731 return len(self.required_fixtures) > 0 

732 

733 def update_callspecs(self): 

734 """ 

735 

736 :return: 

737 """ 

738 for pmark in self.pmarks: 

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

740 if PYTEST3_OR_GREATER: 740 ↛ 743line 740 didn't jump to line 743, because the condition on line 740 was never false

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

742 else: 

743 argvals = [] 

744 for v in pmark.param_values: 

745 if is_marked_parameter_value(v): 

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

747 argvals.append(newmark) 

748 else: 

749 argvals.append((v,)) 

750 argvals = tuple(argvals) 

751 else: 

752 argvals = pmark.param_values 

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

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

755 indirect=False, scope='function') 

756 

757 if not PYTEST33_OR_GREATER: 757 ↛ 760line 757 didn't jump to line 760, because the condition on line 757 was never true

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

759 # noinspection PyProtectedMember 

760 for c in self._calls: 

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

762 

763 

764def add_fixture_params(func, new_names): 

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

766 

767 old_sig = signature(func) 

768 

769 # prepend all new parameters if needed 

770 for n in new_names: 

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

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

773 new_sig = add_signature_parameters(old_sig, 

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

775 

776 assert not isgeneratorfunction(func) 

777 

778 # normal function with return statement 

779 @wraps(func, new_sig=new_sig) 

780 def wrapped_func(**kwargs): 

781 for n in new_names: 

782 kwargs.pop(n) 

783 return func(**kwargs) 

784 

785 # else: 

786 # # generator function (with a yield statement) 

787 # @wraps(fixture_func, new_sig=new_sig) 

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

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

790 # if is_used_request(request): 

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

792 # yield res 

793 # else: 

794 # yield NOT_USED 

795 

796 return wrapped_func 

797 

798 

799def get_callspecs(func): 

800 """ 

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

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

803 

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

805 """ 

806 meta = MiniMetafunc(func) 

807 # meta.update_callspecs() 

808 # noinspection PyProtectedMember 

809 return meta._calls 

810 

811 

812def cart_product_pytest(argnames, argvalues): 

813 """ 

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

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

816 - we also propagate marks 

817 

818 :param argnames: 

819 :param argvalues: 

820 :return: 

821 """ 

822 # transform argnames into a list of lists 

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

824 

825 # make the cartesian product per se 

826 argvalues_prod = _cart_product_pytest(argnames_lists, argvalues) 

827 

828 # flatten the list of argnames 

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

830 

831 # apply all marks to the arvalues 

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

833 for marks, argvalues in argvalues_prod] 

834 

835 return argnames_list, argvalues_prod 

836 

837 

838def _cart_product_pytest(argnames_lists, argvalues): 

839 result = [] 

840 

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

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

843 

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

845 for x in argvalues[0]: 

846 # handle x 

847 nb_names = len(argnames_lists[0]) 

848 

849 # (1) extract meta-info 

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

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

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

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

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

855 

856 # (2) possibly unpack 

857 if nb_names > 1: 

858 # if lazy value, we have to do something 

859 if is_lazy_value(x_value): 

860 x_value_lst = x_value.as_lazy_items_list(nb_names) 

861 else: 

862 x_value_lst = list(x_value) 

863 else: 

864 x_value_lst = [x_value] 

865 

866 # product 

867 if len(argvalues) > 1: 

868 for m, p in sub_product: 

869 # combine marks and values 

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

871 else: 

872 result.append((x_marks_lst, x_value_lst)) 

873 

874 return result 

875 

876 

877def inject_host(apply_decorator): 

878 """ 

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

880 

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

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

883 happens. 

884 

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

886 

887 :param apply_decorator: 

888 :return: 

889 """ 

890 # class _apply_decorator_with_host_tracking(object): 

891 # def __init__(self, _target): 

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

893 # self._target = _target 

894 # self.__wrapped__ = None 

895 # 

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

897 # """ 

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

899 # :param obj: 

900 # :param type_: 

901 # :return: 

902 # """ 

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

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

905 # # order to store fixtures there) 

906 # if self.__wrapped__ is None: 

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

908 # try: 

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

910 # except Exception as e: 

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

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

913 # 

914 # # path, lineno = get_fslocation_from_item(self) 

915 # # warn_explicit( 

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

917 # # category=None, 

918 # # filename=str(path), 

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

920 # # ) 

921 # # 

922 # # @wraps(self._target) 

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

924 # # raise e 

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

926 # # del _exc_raiser.__wrapped__ 

927 # # self.__wrapped__ = _exc_raiser 

928 # 

929 # return self.__wrapped__ 

930 # 

931 # def __getattribute__(self, item): 

932 # if item == '__call__': 

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

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

935 # 

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

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

938 # return self.__call__ 

939 # else: 

940 # return object.__getattribute__(self, item) 

941 # 

942 # return _apply_decorator_with_host_tracking 

943 

944 def apply(test_or_fixture_func): 

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

946 # 

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

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

949 # 

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

951 container = get_function_host(test_or_fixture_func) 

952 return apply_decorator(test_or_fixture_func, container) 

953 

954 return apply 

955 

956 

957def get_pytest_request_and_item(request_or_item): 

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

959 try: 

960 item = request_or_item.node 

961 except AttributeError: 

962 item = request_or_item 

963 request = item._request 

964 else: 

965 request = request_or_item 

966 

967 return item, request