Coverage for src/pytest_cases/common_others.py: 69%

220 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> 

5import functools 

6import inspect 

7from keyword import iskeyword 

8import makefun 

9from importlib import import_module 

10from inspect import findsource 

11import re 

12 

13try: 

14 from typing import Union, Callable, Any, Optional, Tuple, Type # noqa 

15except ImportError: 

16 pass 

17 

18from .common_mini_six import string_types, PY3, PY34 

19 

20 

21def get_code_first_line(f): 

22 """ 

23 Returns the source code associated to function or class f. It is robust to wrappers such as @lru_cache 

24 :param f: 

25 :return: 

26 """ 

27 # todo maybe use inspect.unwrap instead? 

28 if hasattr(f, '__wrapped__'): 28 ↛ 29line 28 didn't jump to line 29, because the condition on line 28 was never true

29 return get_code_first_line(f.__wrapped__) 

30 elif hasattr(f, '__code__'): 

31 # a function 

32 return f.__code__.co_firstlineno 

33 else: 

34 # a class ? 

35 try: 

36 _, lineno = findsource(f) 

37 return lineno 

38 except: # noqa 

39 raise ValueError("Cannot get code information for function or class %r" % f) 

40 

41 

42# Below is the beginning of a switch from our scanning code to the same one than pytest. See `case_parametrizer_new` 

43# from _pytest.compat import get_real_func as compat_get_real_func 

44# 

45# try: 

46# from _pytest._code.source import getfslineno as compat_getfslineno 

47# except ImportError: 

48# from _pytest.compat import getfslineno as compat_getfslineno 

49 

50try: 

51 ExpectedError = Optional[Union[Type[Exception], str, Exception, Callable[[Exception], Optional[bool]]]] 

52 """The expected error in case failure is expected. An exception type, instance, or a validation function""" 

53 

54 ExpectedErrorType = Optional[Type[BaseException]] 

55 ExpectedErrorPattern = Optional[re.Pattern] 

56 ExpectedErrorInstance = Optional[BaseException] 

57 ExpectedErrorValidator = Optional[Callable[[BaseException], Optional[bool]]] 

58 

59except: # noqa 

60 pass 

61 

62 

63def unfold_expected_err(expected_e # type: ExpectedError 

64 ): 

65 # type: (...) -> Tuple[ExpectedErrorType, ExpectedErrorPattern, ExpectedErrorInstance, ExpectedErrorValidator] 

66 """ 

67 'Unfolds' the expected error `expected_e` to return a tuple of 

68 - expected error type 

69 - expected error representation pattern (a regex Pattern) 

70 - expected error instance 

71 - error validation callable 

72 

73 If `expected_e` is an exception type, returns `expected_e, None, None, None` 

74 

75 If `expected_e` is a string, returns `BaseException, re.compile(expected_e), None, None` 

76 

77 If `expected_e` is an exception instance, returns `type(expected_e), None, expected_e, None` 

78 

79 If `expected_e` is an exception validation function, returns `BaseException, None, None, expected_e` 

80 

81 :param expected_e: an `ExpectedError`, that is, either an exception type, a regex string, an exception 

82 instance, or an exception validation function 

83 :return: 

84 """ 

85 if type(expected_e) is type and issubclass(expected_e, BaseException): 

86 return expected_e, None, None, None 

87 

88 elif isinstance(expected_e, string_types): 

89 return BaseException, re.compile(expected_e), None, None # noqa 

90 

91 elif issubclass(type(expected_e), Exception): 91 ↛ 94line 91 didn't jump to line 94, because the condition on line 91 was never false

92 return type(expected_e), None, expected_e, None 

93 

94 elif callable(expected_e): 

95 return BaseException, None, None, expected_e 

96 

97 raise ValueError("ExpectedNormal error should either be an exception type, an exception instance, or an exception " 

98 "validation callable") 

99 

100 

101def assert_exception(expected # type: ExpectedError 

102 ): 

103 """ 

104 A context manager to check that some bit of code raises an exception. Sometimes it might be more 

105 handy than `with pytest.raises():`. 

106 

107 `expected` can be: 

108 

109 - an expected error type, in which case `isinstance(caught, expected)` will be used for validity checking 

110 

111 - an expected error representation pattern (a regex pattern string), in which case 

112 `expected.match(repr(caught))` will be used for validity checking 

113 

114 - an expected error instance, in which case BOTH `isinstance(caught, type(expected))` AND 

115 `caught == expected` will be used for validity checking 

116 

117 - an error validation callable, in which case `expected(caught) is not False` will be used for validity 

118 checking 

119 

120 Upon failure, this raises an `ExceptionCheckingError` (a subclass of `AssertionError`) 

121 

122 ```python 

123 # good type - ok 

124 with assert_exception(ValueError): 

125 raise ValueError() 

126 

127 # good type - inherited - ok 

128 class MyErr(ValueError): 

129 pass 

130 with assert_exception(ValueError): 

131 raise MyErr() 

132 

133 # no exception - raises ExceptionCheckingError 

134 with assert_exception(ValueError): 

135 pass 

136 

137 # wrong type - raises ExceptionCheckingError 

138 with assert_exception(ValueError): 

139 raise TypeError() 

140 

141 # good repr pattern - ok 

142 with assert_exception(r"ValueError\\('hello'[,]+\\)"): 

143 raise ValueError("hello") 

144 

145 # good instance equality check - ok 

146 class MyExc(Exception): 

147 def __eq__(self, other): 

148 return vars(self) == vars(other) 

149 with assert_exception(MyExc('hello')): 

150 raise MyExc("hello") 

151 

152 # good equality but wrong type - raises ExceptionCheckingError 

153 with assert_exception(MyExc('hello')): 

154 raise Exception("hello") 

155 ``` 

156 

157 :param expected: an exception type, instance, repr string pattern, or a callable 

158 """ 

159 return AssertException(expected) 

160 

161 

162class ExceptionCheckingError(AssertionError): 

163 pass 

164 

165 

166class AssertException(object): 

167 """ An implementation of the `assert_exception` context manager""" 

168 

169 __slots__ = ('expected_exception', 'err_type', 'err_ptrn', 'err_inst', 'err_checker') 

170 

171 def __init__(self, expected_exception): 

172 # First see what we need to assert 

173 err_type, err_ptrn, err_inst, err_checker = unfold_expected_err(expected_exception) 

174 self.expected_exception = expected_exception 

175 self.err_type = err_type 

176 self.err_ptrn = err_ptrn 

177 self.err_inst = err_inst 

178 self.err_checker = err_checker 

179 

180 def __enter__(self): 

181 pass 

182 

183 def __exit__(self, exc_type, exc_val, exc_tb): 

184 if exc_type is None: 

185 # bad: no exception caught 

186 raise AssertionError("DID NOT RAISE any BaseException") 

187 

188 # Type check 

189 if not isinstance(exc_val, self.err_type): 

190 raise ExceptionCheckingError("Caught exception %r is not an instance of expected type %r" 

191 % (exc_val, self.err_type)) 

192 

193 # Optional - pattern matching 

194 if self.err_ptrn is not None: 

195 if not self.err_ptrn.match(repr(exc_val)): 195 ↛ 196line 195 didn't jump to line 196, because the condition on line 195 was never true

196 raise ExceptionCheckingError("Caught exception %r does not match expected pattern %r" 

197 % (exc_val, self.err_ptrn)) 

198 

199 # Optional - Additional Exception instance check with equality 

200 if self.err_inst is not None: 

201 # note: do not use != because in python 2 that is not equivalent 

202 if not (exc_val == self.err_inst): 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true

203 raise ExceptionCheckingError("Caught exception %r does not equal expected instance %r" 

204 % (exc_val, self.err_inst)) 

205 

206 # Optional - Additional Exception instance check with custom checker 

207 if self.err_checker is not None: 207 ↛ 208line 207 didn't jump to line 208, because the condition on line 207 was never true

208 if self.err_checker(exc_val) is False: 

209 raise ExceptionCheckingError("Caught exception %r is not valid according to %r" 

210 % (exc_val, self.err_checker)) 

211 

212 # Suppress the exception since it is valid. 

213 # See https://docs.python.org/2/reference/datamodel.html#object.__exit__ 

214 return True 

215 

216 

217AUTO = object() 

218"""Marker for automatic defaults""" 

219 

220 

221def get_host_module(a): 

222 """get the host module of a, or a if it is already a module""" 

223 if inspect.ismodule(a): 

224 return a 

225 else: 

226 return import_module(a.__module__) 

227 

228 

229def in_same_module(a, b): 

230 """Compare the host modules of a and b""" 

231 return get_host_module(a) == get_host_module(b) 

232 

233 

234def get_function_host(func, fallback_to_module=True): 

235 """ 

236 Returns the module or class where func is defined. Approximate method based on qname but "good enough" 

237 

238 :param func: 

239 :param fallback_to_module: if True and an HostNotConstructedYet error is caught, the host module is returned 

240 :return: 

241 """ 

242 host = None 

243 try: 

244 host = get_class_that_defined_method(func) 

245 except HostNotConstructedYet: 

246 # ignore if `fallback_to_module=True` 

247 if not fallback_to_module: 247 ↛ 248line 247 didn't jump to line 248, because the condition on line 247 was never true

248 raise 

249 

250 if host is None: 

251 host = get_host_module(func) 

252 

253 return host 

254 

255 

256def needs_binding(f, return_bound=False): 

257 # type: (...) -> Union[bool, Tuple[bool, Callable]] 

258 """Utility to check if a function needs to be bound to be used """ 

259 

260 # detect non-callables 

261 if isinstance(f, staticmethod): 

262 # only happens if the method is provided as Foo.__dict__['b'], not as Foo.b 

263 # binding is really easy here: pass any class 

264 

265 # no need for the actual class 

266 # bound = f.__get__(get_class_that_defined_method(f.__func__)) 

267 

268 # f.__func__ (python 3) or f.__get__(object) (py2 and py3) work 

269 return (True, f.__get__(object)) if return_bound else True 

270 

271 elif isinstance(f, classmethod): 

272 # only happens if the method is provided as Foo.__dict__['b'], not as Foo.b 

273 if not return_bound: 

274 return True 

275 else: 

276 host_cls = get_class_that_defined_method(f.__func__) 

277 bound = f.__get__(host_cls, host_cls) 

278 return True, bound 

279 

280 else: 

281 # note that for the two above cases callable(f) returns False ! 

282 if not callable(f) and (PY3 or not inspect.ismethoddescriptor(f)): 282 ↛ 283line 282 didn't jump to line 283, because the condition on line 282 was never true

283 raise TypeError("`f` is not a callable !") 

284 

285 if isinstance(f, functools.partial) or fixed_ismethod(f) or is_bound_builtin_method(f): 

286 # already bound, although TODO the functools.partial one is a shortcut that should be analyzed more deeply 

287 return (False, f) if return_bound else False 

288 

289 else: 

290 # can be a static method, a class method, a descriptor... 

291 if not PY3: 291 ↛ 292line 291 didn't jump to line 292, because the condition on line 291 was never true

292 host_cls = getattr(f, "im_class", None) 

293 if host_cls is None: 

294 # defined outside a class: no need for binding 

295 return (False, f) if return_bound else False 

296 else: 

297 bound_obj = getattr(f, "im_self", None) 

298 if bound_obj is None: 

299 # unbound method 

300 if return_bound: 

301 # bind it on an instance 

302 return True, f.__get__(host_cls(), host_cls) # functools.partial(f, host_cls()) 

303 else: 

304 return True 

305 else: 

306 # yes: already bound, no binding needed 

307 return (False, f) if return_bound else False 

308 else: 

309 try: 

310 qname = f.__qualname__ 

311 except AttributeError: 

312 return (False, f) if return_bound else False 

313 else: 

314 if qname == f.__name__: 

315 # not nested - plain old function in a module 

316 return (False, f) if return_bound else False 

317 else: 

318 # NESTED in a class or a function or ... 

319 qname_parts = qname.split(".") 

320 

321 # normal unbound method (since we already eliminated bound ones above with fixed_ismethod(f)) 

322 # or static method accessed on an instance or on a class (!) 

323 # or descriptor-created method 

324 # if "__get__" in qname_parts: 

325 # # a method generated by a descriptor - should be already bound but... 

326 # # 

327 # # see https://docs.python.org/3/reference/datamodel.html#object.__set_name__ 

328 # # The attribute __objclass__ may indicate that an instance of the given type (or a subclass) 

329 # # is expected or required as the first positional argument 

330 # cls_needed = getattr(f, '__objclass__', None) 

331 # if cls_needed is not None: 

332 # return (True, functools.partial(f, cls_needed())) if return_bound else True 

333 # else: 

334 # return (False, f) if return_bound else False 

335 

336 if qname_parts[-2] == "<locals>": 

337 # a function generated by another function. most probably does not require binding 

338 # since `get_class_that_defined_method` does not support those (as PEP3155 states) 

339 # we have no choice but to make this assumption. 

340 return (False, f) if return_bound else False 

341 

342 else: 

343 # unfortunately in order to detect static methods we have no choice: we need the host class 

344 host_cls = get_class_that_defined_method(f) 

345 if host_cls is None: 345 ↛ 346line 345 didn't jump to line 346, because the condition on line 345 was never true

346 get_class_that_defined_method(f) # for debugging, do it again 

347 raise NotImplementedError("This case does not seem covered, please report") 

348 

349 # is it a static method (on instance or class, it is the same), 

350 # an unbound classmethod, or an unbound method ? 

351 # To answer we need to go back to the definition 

352 func_def = inspect.getattr_static(host_cls, f.__name__) 

353 # assert inspect.getattr(host_cls, f.__name__) is f 

354 if isinstance(func_def, staticmethod): 

355 return (False, f) if return_bound else False 

356 elif isinstance(func_def, classmethod): 356 ↛ 358line 356 didn't jump to line 358, because the condition on line 356 was never true

357 # unbound class method 

358 if return_bound: 

359 # bind it on the class 

360 return True, f.__get__(host_cls, host_cls) # functools.partial(f, host_cls) 

361 else: 

362 return True 

363 else: 

364 # unbound method 

365 if return_bound: 

366 # bind it on an instance 

367 return True, f.__get__(host_cls(), host_cls) # functools.partial(f, host_cls()) 

368 else: 

369 return True 

370 

371 

372def is_static_method(cls, func_name, func=None): 

373 """ Adapted from https://stackoverflow.com/a/64436801/7262247 

374 

375 indeed isinstance(staticmethod) does not work if the method is already bound 

376 

377 :param cls: 

378 :param func_name: 

379 :param func: optional, if you have it already 

380 :return: 

381 """ 

382 if func is not None: 

383 assert getattr(cls, func_name) is func 

384 

385 return isinstance(inspect.getattr_static(cls, func_name), staticmethod) 

386 

387 

388def is_class_method(cls, func_name, func=None): 

389 """ Adapted from https://stackoverflow.com/a/64436801/7262247 

390 

391 indeed isinstance(classmethod) does not work if the method is already bound 

392 

393 :param cls: 

394 :param func_name: 

395 :param func: optional, if you have it already 

396 :return: 

397 """ 

398 if func is not None: 

399 assert getattr(cls, func_name) is func 

400 

401 return isinstance(inspect.getattr_static(cls, func_name), classmethod) 

402 

403 

404def is_bound_builtin_method(meth): 

405 """Helper returning True if meth is a bound built-in method""" 

406 return (inspect.isbuiltin(meth) 

407 and getattr(meth, '__self__', None) is not None 

408 and getattr(meth.__self__, '__class__', None)) 

409 

410 

411class HostNotConstructedYet(Exception): 

412 """Raised by `get_class_that_defined_method` in the situation where the host class is not in the host module yet.""" 

413 pass 

414 

415 

416if PY3: 416 ↛ 462line 416 didn't jump to line 462, because the condition on line 416 was never false

417 # this does not need fixing 

418 fixed_ismethod = inspect.ismethod 

419 

420 def get_class_that_defined_method(meth): 

421 """from https://stackoverflow.com/a/25959545/7262247 

422 

423 Improved to support nesting, and to raise an Exception if __qualname__ does 

424 not properly work (instead of returning None which may be misleading) 

425 

426 And yes PEP3155 states that __qualname__ should be used for such introspection. 

427 See https://www.python.org/dev/peps/pep-3155/#rationale 

428 """ 

429 if isinstance(meth, functools.partial): 429 ↛ 430line 429 didn't jump to line 430, because the condition on line 429 was never true

430 return get_class_that_defined_method(meth.func) 

431 

432 if inspect.ismethod(meth) or is_bound_builtin_method(meth): 

433 for cls in inspect.getmro(meth.__self__.__class__): 

434 if meth.__name__ in cls.__dict__: 

435 return cls 

436 meth = getattr(meth, '__func__', meth) # fallback to __qualname__ parsing 

437 

438 if inspect.isfunction(meth): 

439 host = inspect.getmodule(meth) 

440 host_part = meth.__qualname__.split('.<locals>', 1)[0] 

441 # note: the local part of qname is not walkable see https://www.python.org/dev/peps/pep-3155/#limitations 

442 for item in host_part.split('.')[:-1]: 

443 try: 

444 host = getattr(host, item) 

445 except AttributeError: 

446 # non-resolvable __qualname__ 

447 raise HostNotConstructedYet( 

448 "__qualname__ is not resolvable, this can happen if the host class of this method " 

449 "%r has not yet been created. PEP3155 does not seem to tell us what we should do " 

450 "in this case." % meth 

451 ) 

452 if host is None: 452 ↛ 453line 452 didn't jump to line 453, because the condition on line 452 was never true

453 raise ValueError("__qualname__ leads to `None`, this is strange and not PEP3155 compliant, please " 

454 "report") 

455 

456 if isinstance(host, type): 

457 return host 

458 

459 return getattr(meth, '__objclass__', None) # handle special descriptor objects 

460 

461else: 

462 def fixed_ismethod(f): 

463 """inspect.ismethod does not have the same contract in python 2: it returns True even for bound methods""" 

464 return hasattr(f, '__self__') and f.__self__ is not None 

465 

466 def get_class_that_defined_method(meth): 

467 """from https://stackoverflow.com/a/961057/7262247 

468 

469 Adapted to support partial 

470 """ 

471 if isinstance(meth, functools.partial): 

472 return get_class_that_defined_method(meth.func) 

473 

474 try: 

475 _mro = inspect.getmro(meth.im_class) 

476 except AttributeError: 

477 # no host class 

478 return None 

479 else: 

480 for cls in _mro: 

481 if meth.__name__ in cls.__dict__: 

482 return cls 

483 return None 

484 

485 

486if PY3: 486 ↛ 490line 486 didn't jump to line 490, because the condition on line 486 was never false

487 def qname(func): 

488 return func.__qualname__ 

489else: 

490 def qname(func): 

491 """'good enough' python 2 implementation of __qualname__""" 

492 try: 

493 hostclass = func.im_class 

494 except AttributeError: 

495 # no host class 

496 return "%s.%s" % (func.__module__, func.__name__) 

497 else: 

498 # host class: recurse (note that in python 2 nested classes do not have a way to know their parent class) 

499 return "%s.%s" % (qname(hostclass), func.__name__) 

500 

501 

502# if sys.version_info > (3, ): 

503def funcopy(f): 

504 """ 

505 

506 >>> def foo(): 

507 ... return 1 

508 >>> foo.att = 2 

509 >>> f = funcopy(foo) 

510 >>> f.att 

511 2 

512 >>> f() 

513 1 

514 

515 """ 

516 # see https://stackoverflow.com/a/6527746/7262247 

517 # and https://stackoverflow.com/a/13503277/7262247 

518 # apparently it is not possible to create an actual copy with copy() ! 

519 # Use makefun.partial which preserves the parametrization marks (we need them) 

520 return makefun.partial(f) 

521 # fun = FunctionType(f.__code__, f.__globals__, f.__name__, f.__defaults__, f.__closure__) 

522 # fun.__dict__.update(f.__dict__) 

523 # fun = functools.update_wrapper(fun, f) 

524 # fun.__kwdefaults__ = f.__kwdefaults__ 

525 # return fun 

526# else: 

527# def funcopy(f): 

528# fun = FunctionType(f.func_code, f.func_globals, name=f.func_name, argdefs=f.func_defaults, 

529# closure=f.func_closure) 

530# fun.__dict__.update(f.__dict__) 

531# fun = functools.update_wrapper(fun, f) 

532# fun.__kwdefaults__ = f.__kwdefaults__ 

533# return fun 

534 

535 

536def robust_isinstance(o, cls): 

537 try: 

538 return isinstance(o, cls) 

539 except: # noqa 

540 return False 

541 

542 

543def isidentifier(s # type: str 

544 ): 

545 """python 2+3 compliant <str>.isidentifier()""" 

546 try: 

547 return s.isidentifier() 

548 except AttributeError: 

549 return re.match("[a-zA-Z_]\\w*\\Z", s) 

550 

551 

552def make_identifier(name # type: str 

553 ): 

554 """Transform the given name into a valid python identifier""" 

555 if not isinstance(name, string_types): 555 ↛ 556line 555 didn't jump to line 556, because the condition on line 555 was never true

556 raise TypeError("name should be a string, found : %r" % name) 

557 

558 if iskeyword(name) or (not PY3 and name == "None"): 

559 # reserved keywords: add an underscore 

560 name = name + "_" 

561 

562 if isidentifier(name): 

563 return name 

564 elif len(name) == 0: 

565 # empty string 

566 return "_" 

567 else: 

568 # first remove any forbidden character (https://stackoverflow.com/a/3305731/7262247) 

569 # \W : matches any character that is not a word character 

570 new_name = re.sub("\\W+", '_', name) 

571 # then add a leading underscore if needed 

572 # ^(?=\\d) : matches any digit that would be at the beginning of the string 

573 if re.match("^(?=\\d)", new_name): 

574 new_name = "_" + new_name 

575 return new_name 

576 

577 

578if PY34: 578 ↛ 584line 578 didn't jump to line 584, because the condition on line 578 was never false

579 def replace_list_contents(the_list, new_contents): 

580 """Replaces the contents of a list""" 

581 the_list.clear() 

582 the_list.extend(new_contents) 

583else: 

584 def replace_list_contents(the_list, new_contents): 

585 """Replaces the contents of a list""" 

586 del the_list[:] 

587 the_list.extend(new_contents)