reflection.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. # coding=utf-8
  2. #
  3. # This file is part of Hypothesis, which may be found at
  4. # https://github.com/HypothesisWorks/hypothesis-python
  5. #
  6. # Most of this work is copyright (C) 2013-2018 David R. MacIver
  7. # (david@drmaciver.com), but it contains contributions by others. See
  8. # CONTRIBUTING.rst for a full list of people who may hold copyright, and
  9. # consult the git log if you need to determine who owns an individual
  10. # contribution.
  11. #
  12. # This Source Code Form is subject to the terms of the Mozilla Public License,
  13. # v. 2.0. If a copy of the MPL was not distributed with this file, You can
  14. # obtain one at http://mozilla.org/MPL/2.0/.
  15. #
  16. # END HEADER
  17. """This file can approximately be considered the collection of hypothesis going
  18. to really unreasonable lengths to produce pretty output."""
  19. from __future__ import division, print_function, absolute_import
  20. import re
  21. import ast
  22. import uuid
  23. import types
  24. import hashlib
  25. import inspect
  26. from types import ModuleType
  27. from functools import wraps
  28. from hypothesis.configuration import storage_directory
  29. from hypothesis.vendor.pretty import pretty
  30. from hypothesis.internal.compat import ARG_NAME_ATTRIBUTE, hrange, \
  31. to_str, qualname, to_unicode, isidentifier, str_to_bytes, \
  32. getfullargspec, update_code_location
  33. def fully_qualified_name(f):
  34. """Returns a unique identifier for f pointing to the module it was define
  35. on, and an containing functions."""
  36. if f.__module__ is not None:
  37. return f.__module__ + '.' + qualname(f)
  38. else:
  39. return qualname(f)
  40. def is_mock(obj):
  41. """Determine if the given argument is a mock type.
  42. We want to be able to detect these when dealing with various test
  43. args. As they are sneaky and can look like almost anything else,
  44. we'll check this by looking for random attributes. This is more
  45. robust than looking for types.
  46. """
  47. for _ in range(10):
  48. if not hasattr(obj, str(uuid.uuid4())):
  49. return False
  50. return True
  51. def function_digest(function):
  52. """Returns a string that is stable across multiple invocations across
  53. multiple processes and is prone to changing significantly in response to
  54. minor changes to the function.
  55. No guarantee of uniqueness though it usually will be.
  56. """
  57. hasher = hashlib.md5()
  58. try:
  59. hasher.update(to_unicode(inspect.getsource(function)).encode('utf-8'))
  60. # Different errors on different versions of python. What fun.
  61. except (OSError, IOError, TypeError):
  62. pass
  63. try:
  64. hasher.update(str_to_bytes(function.__name__))
  65. except AttributeError:
  66. pass
  67. try:
  68. hasher.update(function.__module__.__name__.encode('utf-8'))
  69. except AttributeError:
  70. pass
  71. try:
  72. hasher.update(str_to_bytes(repr(getfullargspec(function))))
  73. except TypeError:
  74. pass
  75. return hasher.digest()
  76. def required_args(target, args=(), kwargs=()):
  77. """Return a set of names of required args to target that were not supplied
  78. in args or kwargs.
  79. This is used in builds() to determine which arguments to attempt to
  80. fill from type hints. target may be any callable (including classes
  81. and bound methods). args and kwargs should be as they are passed to
  82. builds() - that is, a tuple of values and a dict of names: values.
  83. """
  84. try:
  85. spec = getfullargspec(
  86. target.__init__ if inspect.isclass(target) else target)
  87. except TypeError: # pragma: no cover
  88. return None
  89. # self appears in the argspec of __init__ and bound methods, but it's an
  90. # error to explicitly supply it - so we might skip the first argument.
  91. skip_self = int(inspect.isclass(target) or inspect.ismethod(target))
  92. # Start with the args that were not supplied and all kwonly arguments,
  93. # then remove all positional arguments with default values, and finally
  94. # remove kwonly defaults and any supplied keyword arguments
  95. return set(spec.args[skip_self + len(args):] + spec.kwonlyargs) \
  96. - set(spec.args[len(spec.args) - len(spec.defaults or ()):]) \
  97. - set(spec.kwonlydefaults or ()) - set(kwargs)
  98. def convert_keyword_arguments(function, args, kwargs):
  99. """Returns a pair of a tuple and a dictionary which would be equivalent
  100. passed as positional and keyword args to the function. Unless function has.
  101. **kwargs the dictionary will always be empty.
  102. """
  103. argspec = getfullargspec(function)
  104. new_args = []
  105. kwargs = dict(kwargs)
  106. defaults = dict(argspec.kwonlydefaults or {})
  107. if argspec.defaults:
  108. for name, value in zip(
  109. argspec.args[-len(argspec.defaults):],
  110. argspec.defaults
  111. ):
  112. defaults[name] = value
  113. n = max(len(args), len(argspec.args))
  114. for i in hrange(n):
  115. if i < len(args):
  116. new_args.append(args[i])
  117. else:
  118. arg_name = argspec.args[i]
  119. if arg_name in kwargs:
  120. new_args.append(kwargs.pop(arg_name))
  121. elif arg_name in defaults:
  122. new_args.append(defaults[arg_name])
  123. else:
  124. raise TypeError('No value provided for argument %r' % (
  125. arg_name
  126. ))
  127. if kwargs and not argspec.varkw:
  128. if len(kwargs) > 1:
  129. raise TypeError('%s() got unexpected keyword arguments %s' % (
  130. function.__name__, ', '.join(map(repr, kwargs))
  131. ))
  132. else:
  133. bad_kwarg = next(iter(kwargs))
  134. raise TypeError('%s() got an unexpected keyword argument %r' % (
  135. function.__name__, bad_kwarg
  136. ))
  137. return tuple(new_args), kwargs
  138. def convert_positional_arguments(function, args, kwargs):
  139. """Return a tuple (new_args, new_kwargs) where all possible arguments have
  140. been moved to kwargs.
  141. new_args will only be non-empty if function has a variadic argument.
  142. """
  143. argspec = getfullargspec(function)
  144. new_kwargs = dict(argspec.kwonlydefaults or {})
  145. new_kwargs.update(kwargs)
  146. if not argspec.varkw:
  147. for k in new_kwargs.keys():
  148. if k not in argspec.args and k not in argspec.kwonlyargs:
  149. raise TypeError(
  150. '%s() got an unexpected keyword argument %r' % (
  151. function.__name__, k
  152. ))
  153. if len(args) < len(argspec.args):
  154. for i in hrange(
  155. len(args), len(argspec.args) - len(argspec.defaults or ())
  156. ):
  157. if argspec.args[i] not in kwargs:
  158. raise TypeError('No value provided for argument %s' % (
  159. argspec.args[i],
  160. ))
  161. for kw in argspec.kwonlyargs:
  162. if kw not in new_kwargs:
  163. raise TypeError('No value provided for argument %s' % kw)
  164. if len(args) > len(argspec.args) and not argspec.varargs:
  165. raise TypeError(
  166. '%s() takes at most %d positional arguments (%d given)' % (
  167. function.__name__, len(argspec.args), len(args)
  168. )
  169. )
  170. for arg, name in zip(args, argspec.args):
  171. if name in new_kwargs:
  172. raise TypeError(
  173. '%s() got multiple values for keyword argument %r' % (
  174. function.__name__, name
  175. ))
  176. else:
  177. new_kwargs[name] = arg
  178. return (
  179. tuple(args[len(argspec.args):]),
  180. new_kwargs,
  181. )
  182. def extract_all_lambdas(tree):
  183. lambdas = []
  184. class Visitor(ast.NodeVisitor):
  185. def visit_Lambda(self, node):
  186. lambdas.append(node)
  187. Visitor().visit(tree)
  188. return lambdas
  189. def args_for_lambda_ast(l):
  190. return [getattr(n, ARG_NAME_ATTRIBUTE) for n in l.args.args]
  191. LINE_CONTINUATION = re.compile(r"\\\n")
  192. WHITESPACE = re.compile(r"\s+")
  193. PROBABLY_A_COMMENT = re.compile("""#[^'"]*$""")
  194. SPACE_FOLLOWS_OPEN_BRACKET = re.compile(r"\( ")
  195. SPACE_PRECEDES_CLOSE_BRACKET = re.compile(r" \)")
  196. def extract_lambda_source(f):
  197. """Extracts a single lambda expression from the string source. Returns a
  198. string indicating an unknown body if it gets confused in any way.
  199. This is not a good function and I am sorry for it. Forgive me my
  200. sins, oh lord
  201. """
  202. argspec = getfullargspec(f)
  203. arg_strings = []
  204. # In Python 2 you can have destructuring arguments to functions. This
  205. # results in an argspec with non-string values. I'm not very interested in
  206. # handling these properly, but it's important to not crash on them.
  207. bad_lambda = False
  208. for a in argspec.args:
  209. if isinstance(a, (tuple, list)): # pragma: no cover
  210. arg_strings.append('(%s)' % (', '.join(a),))
  211. bad_lambda = True
  212. else:
  213. assert isinstance(a, str)
  214. arg_strings.append(a)
  215. if argspec.varargs:
  216. arg_strings.append('*' + argspec.varargs)
  217. elif argspec.kwonlyargs:
  218. arg_strings.append('*')
  219. for a in (argspec.kwonlyargs or []):
  220. default = (argspec.kwonlydefaults or {}).get(a)
  221. if default:
  222. arg_strings.append('{}={}'.format(a, default))
  223. else:
  224. arg_strings.append(a)
  225. if_confused = 'lambda %s: <unknown>' % (', '.join(arg_strings),)
  226. if bad_lambda: # pragma: no cover
  227. return if_confused
  228. try:
  229. source = inspect.getsource(f)
  230. except IOError:
  231. return if_confused
  232. source = LINE_CONTINUATION.sub(' ', source)
  233. source = WHITESPACE.sub(' ', source)
  234. source = source.strip()
  235. assert 'lambda' in source
  236. tree = None
  237. try:
  238. tree = ast.parse(source)
  239. except SyntaxError:
  240. for i in hrange(len(source) - 1, len('lambda'), -1):
  241. prefix = source[:i]
  242. if 'lambda' not in prefix:
  243. break
  244. try:
  245. tree = ast.parse(prefix)
  246. source = prefix
  247. break
  248. except SyntaxError:
  249. continue
  250. if tree is None:
  251. if source.startswith('@'):
  252. # This will always eventually find a valid expression because
  253. # the decorator must be a valid Python function call, so will
  254. # eventually be syntactically valid and break out of the loop. Thus
  255. # this loop can never terminate normally, so a no branch pragma is
  256. # appropriate.
  257. for i in hrange(len(source) + 1): # pragma: no branch
  258. p = source[1:i]
  259. if 'lambda' in p:
  260. try:
  261. tree = ast.parse(p)
  262. source = p
  263. break
  264. except SyntaxError:
  265. pass
  266. if tree is None:
  267. return if_confused
  268. all_lambdas = extract_all_lambdas(tree)
  269. aligned_lambdas = [
  270. l for l in all_lambdas
  271. if args_for_lambda_ast(l) == argspec.args
  272. ]
  273. if len(aligned_lambdas) != 1:
  274. return if_confused
  275. lambda_ast = aligned_lambdas[0]
  276. assert lambda_ast.lineno == 1
  277. source = source[lambda_ast.col_offset:].strip()
  278. source = source[source.index('lambda'):]
  279. for i in hrange(len(source), len('lambda'), -1): # pragma: no branch
  280. try:
  281. parsed = ast.parse(source[:i])
  282. assert len(parsed.body) == 1
  283. assert parsed.body
  284. if isinstance(parsed.body[0].value, ast.Lambda):
  285. source = source[:i]
  286. break
  287. except SyntaxError:
  288. pass
  289. lines = source.split('\n')
  290. lines = [PROBABLY_A_COMMENT.sub('', l) for l in lines]
  291. source = '\n'.join(lines)
  292. source = WHITESPACE.sub(' ', source)
  293. source = SPACE_FOLLOWS_OPEN_BRACKET.sub('(', source)
  294. source = SPACE_PRECEDES_CLOSE_BRACKET.sub(')', source)
  295. source = source.strip()
  296. return source
  297. def get_pretty_function_description(f):
  298. if not hasattr(f, '__name__'):
  299. return repr(f)
  300. name = f.__name__
  301. if name == '<lambda>':
  302. result = extract_lambda_source(f)
  303. return result
  304. elif isinstance(f, types.MethodType):
  305. self = f.__self__
  306. if not (self is None or inspect.isclass(self)):
  307. return '%r.%s' % (self, name)
  308. return name
  309. def nicerepr(v):
  310. if inspect.isfunction(v):
  311. return get_pretty_function_description(v)
  312. elif isinstance(v, type):
  313. return v.__name__
  314. else:
  315. return to_str(pretty(v))
  316. def arg_string(f, args, kwargs, reorder=True):
  317. if reorder:
  318. args, kwargs = convert_positional_arguments(f, args, kwargs)
  319. argspec = getfullargspec(f)
  320. bits = []
  321. for a in argspec.args:
  322. if a in kwargs:
  323. bits.append('%s=%s' % (a, nicerepr(kwargs.pop(a))))
  324. if kwargs:
  325. for a in sorted(kwargs):
  326. bits.append('%s=%s' % (a, nicerepr(kwargs[a])))
  327. return ', '.join([nicerepr(x) for x in args] + bits)
  328. def unbind_method(f):
  329. """Take something that might be a method or a function and return the
  330. underlying function."""
  331. return getattr(f, 'im_func', getattr(f, '__func__', f))
  332. def check_valid_identifier(identifier):
  333. if not isidentifier(identifier):
  334. raise ValueError('%r is not a valid python identifier' %
  335. (identifier,))
  336. def eval_directory():
  337. return storage_directory('eval_source')
  338. eval_cache = {}
  339. def source_exec_as_module(source):
  340. try:
  341. return eval_cache[source]
  342. except KeyError:
  343. pass
  344. result = ModuleType('hypothesis_temporary_module_%s' % (
  345. hashlib.sha1(str_to_bytes(source)).hexdigest(),
  346. ))
  347. assert isinstance(source, str)
  348. exec(source, result.__dict__)
  349. eval_cache[source] = result
  350. return result
  351. COPY_ARGSPEC_SCRIPT = """
  352. from hypothesis.utils.conventions import not_set
  353. def accept(%(funcname)s):
  354. def %(name)s(%(argspec)s):
  355. return %(funcname)s(%(invocation)s)
  356. return %(name)s
  357. """.strip() + '\n'
  358. def define_function_signature(name, docstring, argspec):
  359. """A decorator which sets the name, argspec and docstring of the function
  360. passed into it."""
  361. check_valid_identifier(name)
  362. for a in argspec.args:
  363. check_valid_identifier(a)
  364. if argspec.varargs is not None:
  365. check_valid_identifier(argspec.varargs)
  366. if argspec.varkw is not None:
  367. check_valid_identifier(argspec.varkw)
  368. n_defaults = len(argspec.defaults or ())
  369. if n_defaults:
  370. parts = []
  371. for a in argspec.args[:-n_defaults]:
  372. parts.append(a)
  373. for a in argspec.args[-n_defaults:]:
  374. parts.append('%s=not_set' % (a,))
  375. else:
  376. parts = list(argspec.args)
  377. used_names = list(argspec.args) + list(argspec.kwonlyargs)
  378. used_names.append(name)
  379. for a in argspec.kwonlyargs:
  380. check_valid_identifier(a)
  381. def accept(f):
  382. fargspec = getfullargspec(f)
  383. must_pass_as_kwargs = []
  384. invocation_parts = []
  385. for a in argspec.args:
  386. if a not in fargspec.args and not fargspec.varargs:
  387. must_pass_as_kwargs.append(a)
  388. else:
  389. invocation_parts.append(a)
  390. if argspec.varargs:
  391. used_names.append(argspec.varargs)
  392. parts.append('*' + argspec.varargs)
  393. invocation_parts.append('*' + argspec.varargs)
  394. elif argspec.kwonlyargs:
  395. parts.append('*')
  396. for k in must_pass_as_kwargs:
  397. invocation_parts.append('%(k)s=%(k)s' % {'k': k})
  398. for k in argspec.kwonlyargs:
  399. invocation_parts.append('%(k)s=%(k)s' % {'k': k})
  400. if k in (argspec.kwonlydefaults or []):
  401. parts.append('%(k)s=not_set' % {'k': k})
  402. else:
  403. parts.append(k)
  404. if argspec.varkw:
  405. used_names.append(argspec.varkw)
  406. parts.append('**' + argspec.varkw)
  407. invocation_parts.append('**' + argspec.varkw)
  408. candidate_names = ['f'] + [
  409. 'f_%d' % (i,) for i in hrange(1, len(used_names) + 2)
  410. ]
  411. for funcname in candidate_names: # pragma: no branch
  412. if funcname not in used_names:
  413. break
  414. base_accept = source_exec_as_module(
  415. COPY_ARGSPEC_SCRIPT % {
  416. 'name': name,
  417. 'funcname': funcname,
  418. 'argspec': ', '.join(parts),
  419. 'invocation': ', '.join(invocation_parts)
  420. }).accept
  421. result = base_accept(f)
  422. result.__doc__ = docstring
  423. result.__defaults__ = argspec.defaults
  424. if argspec.kwonlydefaults:
  425. result.__kwdefaults__ = argspec.kwonlydefaults
  426. if argspec.annotations:
  427. result.__annotations__ = argspec.annotations
  428. return result
  429. return accept
  430. def impersonate(target):
  431. """Decorator to update the attributes of a function so that to external
  432. introspectors it will appear to be the target function.
  433. Note that this updates the function in place, it doesn't return a
  434. new one.
  435. """
  436. def accept(f):
  437. f.__code__ = update_code_location(
  438. f.__code__,
  439. target.__code__.co_filename, target.__code__.co_firstlineno
  440. )
  441. f.__name__ = target.__name__
  442. f.__module__ = target.__module__
  443. f.__doc__ = target.__doc__
  444. return f
  445. return accept
  446. def proxies(target):
  447. def accept(proxy):
  448. return impersonate(target)(wraps(target)(define_function_signature(
  449. target.__name__, target.__doc__, getfullargspec(target))(proxy)))
  450. return accept