test_func_inspect.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. """
  2. Test the func_inspect module.
  3. """
  4. # Author: Gael Varoquaux <gael dot varoquaux at normalesup dot org>
  5. # Copyright (c) 2009 Gael Varoquaux
  6. # License: BSD Style, 3 clauses.
  7. import functools
  8. from joblib.func_inspect import filter_args, get_func_name, get_func_code
  9. from joblib.func_inspect import _clean_win_chars, format_signature
  10. from joblib.memory import Memory
  11. from joblib.test.common import with_numpy
  12. from joblib.testing import fixture, parametrize, raises
  13. ###############################################################################
  14. # Module-level functions and fixture, for tests
  15. def f(x, y=0):
  16. pass
  17. def g(x):
  18. pass
  19. def h(x, y=0, *args, **kwargs):
  20. pass
  21. def i(x=1):
  22. pass
  23. def j(x, y, **kwargs):
  24. pass
  25. def k(*args, **kwargs):
  26. pass
  27. def m1(x, *, y):
  28. pass
  29. def m2(x, *, y, z=3):
  30. pass
  31. @fixture(scope='module')
  32. def cached_func(tmpdir_factory):
  33. # Create a Memory object to test decorated functions.
  34. # We should be careful not to call the decorated functions, so that
  35. # cache directories are not created in the temp dir.
  36. cachedir = tmpdir_factory.mktemp("joblib_test_func_inspect")
  37. mem = Memory(cachedir.strpath)
  38. @mem.cache
  39. def cached_func_inner(x):
  40. return x
  41. return cached_func_inner
  42. class Klass(object):
  43. def f(self, x):
  44. return x
  45. ###############################################################################
  46. # Tests
  47. @parametrize('func,args,filtered_args',
  48. [(f, [[], (1, )], {'x': 1, 'y': 0}),
  49. (f, [['x'], (1, )], {'y': 0}),
  50. (f, [['y'], (0, )], {'x': 0}),
  51. (f, [['y'], (0, ), {'y': 1}], {'x': 0}),
  52. (f, [['x', 'y'], (0, )], {}),
  53. (f, [[], (0,), {'y': 1}], {'x': 0, 'y': 1}),
  54. (f, [['y'], (), {'x': 2, 'y': 1}], {'x': 2}),
  55. (g, [[], (), {'x': 1}], {'x': 1}),
  56. (i, [[], (2, )], {'x': 2})])
  57. def test_filter_args(func, args, filtered_args):
  58. assert filter_args(func, *args) == filtered_args
  59. def test_filter_args_method():
  60. obj = Klass()
  61. assert filter_args(obj.f, [], (1, )) == {'x': 1, 'self': obj}
  62. @parametrize('func,args,filtered_args',
  63. [(h, [[], (1, )],
  64. {'x': 1, 'y': 0, '*': [], '**': {}}),
  65. (h, [[], (1, 2, 3, 4)],
  66. {'x': 1, 'y': 2, '*': [3, 4], '**': {}}),
  67. (h, [[], (1, 25), {'ee': 2}],
  68. {'x': 1, 'y': 25, '*': [], '**': {'ee': 2}}),
  69. (h, [['*'], (1, 2, 25), {'ee': 2}],
  70. {'x': 1, 'y': 2, '**': {'ee': 2}})])
  71. def test_filter_varargs(func, args, filtered_args):
  72. assert filter_args(func, *args) == filtered_args
  73. test_filter_kwargs_extra_params = [
  74. (m1, [[], (1,), {'y': 2}], {'x': 1, 'y': 2}),
  75. (m2, [[], (1,), {'y': 2}], {'x': 1, 'y': 2, 'z': 3})
  76. ]
  77. @parametrize('func,args,filtered_args',
  78. [(k, [[], (1, 2), {'ee': 2}],
  79. {'*': [1, 2], '**': {'ee': 2}}),
  80. (k, [[], (3, 4)],
  81. {'*': [3, 4], '**': {}})] +
  82. test_filter_kwargs_extra_params)
  83. def test_filter_kwargs(func, args, filtered_args):
  84. assert filter_args(func, *args) == filtered_args
  85. def test_filter_args_2():
  86. assert (filter_args(j, [], (1, 2), {'ee': 2}) ==
  87. {'x': 1, 'y': 2, '**': {'ee': 2}})
  88. ff = functools.partial(f, 1)
  89. # filter_args has to special-case partial
  90. assert filter_args(ff, [], (1, )) == {'*': [1], '**': {}}
  91. assert filter_args(ff, ['y'], (1, )) == {'*': [1], '**': {}}
  92. @parametrize('func,funcname', [(f, 'f'), (g, 'g'),
  93. (cached_func, 'cached_func')])
  94. def test_func_name(func, funcname):
  95. # Check that we are not confused by decoration
  96. # here testcase 'cached_func' is the function itself
  97. assert get_func_name(func)[1] == funcname
  98. def test_func_name_on_inner_func(cached_func):
  99. # Check that we are not confused by decoration
  100. # here testcase 'cached_func' is the 'cached_func_inner' function
  101. # returned by 'cached_func' fixture
  102. assert get_func_name(cached_func)[1] == 'cached_func_inner'
  103. def test_func_inspect_errors():
  104. # Check that func_inspect is robust and will work on weird objects
  105. assert get_func_name('a'.lower)[-1] == 'lower'
  106. assert get_func_code('a'.lower)[1:] == (None, -1)
  107. ff = lambda x: x
  108. assert get_func_name(ff, win_characters=False)[-1] == '<lambda>'
  109. assert get_func_code(ff)[1] == __file__.replace('.pyc', '.py')
  110. # Simulate a function defined in __main__
  111. ff.__module__ = '__main__'
  112. assert get_func_name(ff, win_characters=False)[-1] == '<lambda>'
  113. assert get_func_code(ff)[1] == __file__.replace('.pyc', '.py')
  114. def func_with_kwonly_args(a, b, *, kw1='kw1', kw2='kw2'):
  115. pass
  116. def func_with_signature(a: int, b: int) -> None:
  117. pass
  118. def test_filter_args_edge_cases():
  119. assert (
  120. filter_args(func_with_kwonly_args, [], (1, 2),
  121. {'kw1': 3, 'kw2': 4}) ==
  122. {'a': 1, 'b': 2, 'kw1': 3, 'kw2': 4})
  123. # filter_args doesn't care about keyword-only arguments so you
  124. # can pass 'kw1' into *args without any problem
  125. with raises(ValueError) as excinfo:
  126. filter_args(func_with_kwonly_args, [], (1, 2, 3), {'kw2': 2})
  127. excinfo.match("Keyword-only parameter 'kw1' was passed as positional "
  128. "parameter")
  129. assert (
  130. filter_args(func_with_kwonly_args, ['b', 'kw2'], (1, 2),
  131. {'kw1': 3, 'kw2': 4}) ==
  132. {'a': 1, 'kw1': 3})
  133. assert (filter_args(func_with_signature, ['b'], (1, 2)) == {'a': 1})
  134. def test_bound_methods():
  135. """ Make sure that calling the same method on two different instances
  136. of the same class does resolv to different signatures.
  137. """
  138. a = Klass()
  139. b = Klass()
  140. assert filter_args(a.f, [], (1, )) != filter_args(b.f, [], (1, ))
  141. @parametrize('exception,regex,func,args',
  142. [(ValueError, 'ignore_lst must be a list of parameters to ignore',
  143. f, ['bar', (None, )]),
  144. (ValueError, r'Ignore list: argument \'(.*)\' is not defined',
  145. g, [['bar'], (None, )]),
  146. (ValueError, 'Wrong number of arguments',
  147. h, [[]])])
  148. def test_filter_args_error_msg(exception, regex, func, args):
  149. """ Make sure that filter_args returns decent error messages, for the
  150. sake of the user.
  151. """
  152. with raises(exception) as excinfo:
  153. filter_args(func, *args)
  154. excinfo.match(regex)
  155. def test_filter_args_no_kwargs_mutation():
  156. """None-regression test against 0.12.0 changes.
  157. https://github.com/joblib/joblib/pull/75
  158. Make sure filter args doesn't mutate the kwargs dict that gets passed in.
  159. """
  160. kwargs = {'x': 0}
  161. filter_args(g, [], [], kwargs)
  162. assert kwargs == {'x': 0}
  163. def test_clean_win_chars():
  164. string = r'C:\foo\bar\main.py'
  165. mangled_string = _clean_win_chars(string)
  166. for char in ('\\', ':', '<', '>', '!'):
  167. assert char not in mangled_string
  168. @parametrize('func,args,kwargs,sgn_expected',
  169. [(g, [list(range(5))], {}, 'g([0, 1, 2, 3, 4])'),
  170. (k, [1, 2, (3, 4)], {'y': True}, 'k(1, 2, (3, 4), y=True)')])
  171. def test_format_signature(func, args, kwargs, sgn_expected):
  172. # Test signature formatting.
  173. path, sgn_result = format_signature(func, *args, **kwargs)
  174. assert sgn_result == sgn_expected
  175. def test_format_signature_long_arguments():
  176. shortening_threshold = 1500
  177. # shortening gets it down to 700 characters but there is the name
  178. # of the function in the signature and a few additional things
  179. # like dots for the ellipsis
  180. shortening_target = 700 + 10
  181. arg = 'a' * shortening_threshold
  182. _, signature = format_signature(h, arg)
  183. assert len(signature) < shortening_target
  184. nb_args = 5
  185. args = [arg for _ in range(nb_args)]
  186. _, signature = format_signature(h, *args)
  187. assert len(signature) < shortening_target * nb_args
  188. kwargs = {str(i): arg for i, arg in enumerate(args)}
  189. _, signature = format_signature(h, **kwargs)
  190. assert len(signature) < shortening_target * nb_args
  191. _, signature = format_signature(h, *args, **kwargs)
  192. assert len(signature) < shortening_target * 2 * nb_args
  193. @with_numpy
  194. def test_format_signature_numpy():
  195. """ Test the format signature formatting with numpy.
  196. """
  197. def test_special_source_encoding():
  198. from joblib.test.test_func_inspect_special_encoding import big5_f
  199. func_code, source_file, first_line = get_func_code(big5_f)
  200. assert first_line == 5
  201. assert "def big5_f():" in func_code
  202. assert "test_func_inspect_special_encoding" in source_file
  203. def _get_code():
  204. from joblib.test.test_func_inspect_special_encoding import big5_f
  205. return get_func_code(big5_f)[0]
  206. def test_func_code_consistency():
  207. from joblib.parallel import Parallel, delayed
  208. codes = Parallel(n_jobs=2)(delayed(_get_code)() for _ in range(5))
  209. assert len(set(codes)) == 1