⬅ pytest_cases/common_others.py source

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>
5 import functools
6 import inspect
7 from keyword import iskeyword
8 import makefun
9 from importlib import import_module
10 from inspect import findsource
11 import re
12  
13 try:
14 from typing import Union, Callable, Any, Optional, Tuple, Type # noqa
15 except ImportError:
16 pass
17  
18 from .common_mini_six import string_types, PY3, PY34
19  
20  
21 def 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__'):
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  
50 try:
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  
59 except: # noqa
60 pass
61  
62  
63 def 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):
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  
101 def 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  
162 class ExceptionCheckingError(AssertionError):
163 pass
164  
165  
166 class 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)):
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):
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:
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  
217 AUTO = object()
218 """Marker for automatic defaults"""
219  
220  
221 def 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  
229 def 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  
234 def 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:
248 raise
249  
250 if host is None:
251 host = get_host_module(func)
252  
253 return host
254  
255  
256 def 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)):
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:
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:
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):
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  
372 def 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:
  • S101 Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
383 assert getattr(cls, func_name) is func
384  
385 return isinstance(inspect.getattr_static(cls, func_name), staticmethod)
386  
387  
388 def 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:
  • S101 Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
399 assert getattr(cls, func_name) is func
400  
401 return isinstance(inspect.getattr_static(cls, func_name), classmethod)
402  
403  
404 def 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  
411 class 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  
416 if PY3:
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):
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:
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  
461 else:
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  
486 if PY3:
487 def qname(func):
488 return func.__qualname__
489 else:
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, ):
503 def 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  
536 def robust_isinstance(o, cls):
537 try:
538 return isinstance(o, cls)
539 except: # noqa
540 return False
541  
542  
543 def 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  
552 def make_identifier(name # type: str
553 ):
554 """Transform the given name into a valid python identifier"""
555 if not isinstance(name, string_types):
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  
578 if PY34:
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)
583 else:
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)