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
« 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
13try:
14 from typing import Union, Callable, Any, Optional, Tuple, Type # noqa
15except ImportError:
16 pass
18from .common_mini_six import string_types, PY3, PY34
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)
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
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"""
54 ExpectedErrorType = Optional[Type[BaseException]]
55 ExpectedErrorPattern = Optional[re.Pattern]
56 ExpectedErrorInstance = Optional[BaseException]
57 ExpectedErrorValidator = Optional[Callable[[BaseException], Optional[bool]]]
59except: # noqa
60 pass
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
73 If `expected_e` is an exception type, returns `expected_e, None, None, None`
75 If `expected_e` is a string, returns `BaseException, re.compile(expected_e), None, None`
77 If `expected_e` is an exception instance, returns `type(expected_e), None, expected_e, None`
79 If `expected_e` is an exception validation function, returns `BaseException, None, None, expected_e`
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
88 elif isinstance(expected_e, string_types):
89 return BaseException, re.compile(expected_e), None, None # noqa
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
94 elif callable(expected_e):
95 return BaseException, None, None, expected_e
97 raise ValueError("ExpectedNormal error should either be an exception type, an exception instance, or an exception "
98 "validation callable")
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():`.
107 `expected` can be:
109 - an expected error type, in which case `isinstance(caught, expected)` will be used for validity checking
111 - an expected error representation pattern (a regex pattern string), in which case
112 `expected.match(repr(caught))` will be used for validity checking
114 - an expected error instance, in which case BOTH `isinstance(caught, type(expected))` AND
115 `caught == expected` will be used for validity checking
117 - an error validation callable, in which case `expected(caught) is not False` will be used for validity
118 checking
120 Upon failure, this raises an `ExceptionCheckingError` (a subclass of `AssertionError`)
122 ```python
123 # good type - ok
124 with assert_exception(ValueError):
125 raise ValueError()
127 # good type - inherited - ok
128 class MyErr(ValueError):
129 pass
130 with assert_exception(ValueError):
131 raise MyErr()
133 # no exception - raises ExceptionCheckingError
134 with assert_exception(ValueError):
135 pass
137 # wrong type - raises ExceptionCheckingError
138 with assert_exception(ValueError):
139 raise TypeError()
141 # good repr pattern - ok
142 with assert_exception(r"ValueError\\('hello'[,]+\\)"):
143 raise ValueError("hello")
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")
152 # good equality but wrong type - raises ExceptionCheckingError
153 with assert_exception(MyExc('hello')):
154 raise Exception("hello")
155 ```
157 :param expected: an exception type, instance, repr string pattern, or a callable
158 """
159 return AssertException(expected)
162class ExceptionCheckingError(AssertionError):
163 pass
166class AssertException(object):
167 """ An implementation of the `assert_exception` context manager"""
169 __slots__ = ('expected_exception', 'err_type', 'err_ptrn', 'err_inst', 'err_checker')
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
180 def __enter__(self):
181 pass
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")
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))
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))
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))
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))
212 # Suppress the exception since it is valid.
213 # See https://docs.python.org/2/reference/datamodel.html#object.__exit__
214 return True
217AUTO = object()
218"""Marker for automatic defaults"""
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__)
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)
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"
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
250 if host is None:
251 host = get_host_module(func)
253 return host
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 """
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
265 # no need for the actual class
266 # bound = f.__get__(get_class_that_defined_method(f.__func__))
268 # f.__func__ (python 3) or f.__get__(object) (py2 and py3) work
269 return (True, f.__get__(object)) if return_bound else True
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
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 !")
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
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(".")
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
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
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")
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
372def is_static_method(cls, func_name, func=None):
373 """ Adapted from https://stackoverflow.com/a/64436801/7262247
375 indeed isinstance(staticmethod) does not work if the method is already bound
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
385 return isinstance(inspect.getattr_static(cls, func_name), staticmethod)
388def is_class_method(cls, func_name, func=None):
389 """ Adapted from https://stackoverflow.com/a/64436801/7262247
391 indeed isinstance(classmethod) does not work if the method is already bound
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
401 return isinstance(inspect.getattr_static(cls, func_name), classmethod)
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))
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
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
420 def get_class_that_defined_method(meth):
421 """from https://stackoverflow.com/a/25959545/7262247
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)
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)
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
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")
456 if isinstance(host, type):
457 return host
459 return getattr(meth, '__objclass__', None) # handle special descriptor objects
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
466 def get_class_that_defined_method(meth):
467 """from https://stackoverflow.com/a/961057/7262247
469 Adapted to support partial
470 """
471 if isinstance(meth, functools.partial):
472 return get_class_that_defined_method(meth.func)
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
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__)
502# if sys.version_info > (3, ):
503def funcopy(f):
504 """
506 >>> def foo():
507 ... return 1
508 >>> foo.att = 2
509 >>> f = funcopy(foo)
510 >>> f.att
511 2
512 >>> f()
513 1
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
536def robust_isinstance(o, cls):
537 try:
538 return isinstance(o, cls)
539 except: # noqa
540 return False
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)
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)
558 if iskeyword(name) or (not PY3 and name == "None"):
559 # reserved keywords: add an underscore
560 name = name + "_"
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
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)