123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545 |
- # coding=utf-8
- #
- # This file is part of Hypothesis, which may be found at
- # https://github.com/HypothesisWorks/hypothesis-python
- #
- # Most of this work is copyright (C) 2013-2018 David R. MacIver
- # (david@drmaciver.com), but it contains contributions by others. See
- # CONTRIBUTING.rst for a full list of people who may hold copyright, and
- # consult the git log if you need to determine who owns an individual
- # contribution.
- #
- # This Source Code Form is subject to the terms of the Mozilla Public License,
- # v. 2.0. If a copy of the MPL was not distributed with this file, You can
- # obtain one at http://mozilla.org/MPL/2.0/.
- #
- # END HEADER
- """This file can approximately be considered the collection of hypothesis going
- to really unreasonable lengths to produce pretty output."""
- from __future__ import division, print_function, absolute_import
- import re
- import ast
- import uuid
- import types
- import hashlib
- import inspect
- from types import ModuleType
- from functools import wraps
- from hypothesis.configuration import storage_directory
- from hypothesis.vendor.pretty import pretty
- from hypothesis.internal.compat import ARG_NAME_ATTRIBUTE, hrange, \
- to_str, qualname, to_unicode, isidentifier, str_to_bytes, \
- getfullargspec, update_code_location
- def fully_qualified_name(f):
- """Returns a unique identifier for f pointing to the module it was define
- on, and an containing functions."""
- if f.__module__ is not None:
- return f.__module__ + '.' + qualname(f)
- else:
- return qualname(f)
- def is_mock(obj):
- """Determine if the given argument is a mock type.
- We want to be able to detect these when dealing with various test
- args. As they are sneaky and can look like almost anything else,
- we'll check this by looking for random attributes. This is more
- robust than looking for types.
- """
- for _ in range(10):
- if not hasattr(obj, str(uuid.uuid4())):
- return False
- return True
- def function_digest(function):
- """Returns a string that is stable across multiple invocations across
- multiple processes and is prone to changing significantly in response to
- minor changes to the function.
- No guarantee of uniqueness though it usually will be.
- """
- hasher = hashlib.md5()
- try:
- hasher.update(to_unicode(inspect.getsource(function)).encode('utf-8'))
- # Different errors on different versions of python. What fun.
- except (OSError, IOError, TypeError):
- pass
- try:
- hasher.update(str_to_bytes(function.__name__))
- except AttributeError:
- pass
- try:
- hasher.update(function.__module__.__name__.encode('utf-8'))
- except AttributeError:
- pass
- try:
- hasher.update(str_to_bytes(repr(getfullargspec(function))))
- except TypeError:
- pass
- return hasher.digest()
- def required_args(target, args=(), kwargs=()):
- """Return a set of names of required args to target that were not supplied
- in args or kwargs.
- This is used in builds() to determine which arguments to attempt to
- fill from type hints. target may be any callable (including classes
- and bound methods). args and kwargs should be as they are passed to
- builds() - that is, a tuple of values and a dict of names: values.
- """
- try:
- spec = getfullargspec(
- target.__init__ if inspect.isclass(target) else target)
- except TypeError: # pragma: no cover
- return None
- # self appears in the argspec of __init__ and bound methods, but it's an
- # error to explicitly supply it - so we might skip the first argument.
- skip_self = int(inspect.isclass(target) or inspect.ismethod(target))
- # Start with the args that were not supplied and all kwonly arguments,
- # then remove all positional arguments with default values, and finally
- # remove kwonly defaults and any supplied keyword arguments
- return set(spec.args[skip_self + len(args):] + spec.kwonlyargs) \
- - set(spec.args[len(spec.args) - len(spec.defaults or ()):]) \
- - set(spec.kwonlydefaults or ()) - set(kwargs)
- def convert_keyword_arguments(function, args, kwargs):
- """Returns a pair of a tuple and a dictionary which would be equivalent
- passed as positional and keyword args to the function. Unless function has.
- **kwargs the dictionary will always be empty.
- """
- argspec = getfullargspec(function)
- new_args = []
- kwargs = dict(kwargs)
- defaults = dict(argspec.kwonlydefaults or {})
- if argspec.defaults:
- for name, value in zip(
- argspec.args[-len(argspec.defaults):],
- argspec.defaults
- ):
- defaults[name] = value
- n = max(len(args), len(argspec.args))
- for i in hrange(n):
- if i < len(args):
- new_args.append(args[i])
- else:
- arg_name = argspec.args[i]
- if arg_name in kwargs:
- new_args.append(kwargs.pop(arg_name))
- elif arg_name in defaults:
- new_args.append(defaults[arg_name])
- else:
- raise TypeError('No value provided for argument %r' % (
- arg_name
- ))
- if kwargs and not argspec.varkw:
- if len(kwargs) > 1:
- raise TypeError('%s() got unexpected keyword arguments %s' % (
- function.__name__, ', '.join(map(repr, kwargs))
- ))
- else:
- bad_kwarg = next(iter(kwargs))
- raise TypeError('%s() got an unexpected keyword argument %r' % (
- function.__name__, bad_kwarg
- ))
- return tuple(new_args), kwargs
- def convert_positional_arguments(function, args, kwargs):
- """Return a tuple (new_args, new_kwargs) where all possible arguments have
- been moved to kwargs.
- new_args will only be non-empty if function has a variadic argument.
- """
- argspec = getfullargspec(function)
- new_kwargs = dict(argspec.kwonlydefaults or {})
- new_kwargs.update(kwargs)
- if not argspec.varkw:
- for k in new_kwargs.keys():
- if k not in argspec.args and k not in argspec.kwonlyargs:
- raise TypeError(
- '%s() got an unexpected keyword argument %r' % (
- function.__name__, k
- ))
- if len(args) < len(argspec.args):
- for i in hrange(
- len(args), len(argspec.args) - len(argspec.defaults or ())
- ):
- if argspec.args[i] not in kwargs:
- raise TypeError('No value provided for argument %s' % (
- argspec.args[i],
- ))
- for kw in argspec.kwonlyargs:
- if kw not in new_kwargs:
- raise TypeError('No value provided for argument %s' % kw)
- if len(args) > len(argspec.args) and not argspec.varargs:
- raise TypeError(
- '%s() takes at most %d positional arguments (%d given)' % (
- function.__name__, len(argspec.args), len(args)
- )
- )
- for arg, name in zip(args, argspec.args):
- if name in new_kwargs:
- raise TypeError(
- '%s() got multiple values for keyword argument %r' % (
- function.__name__, name
- ))
- else:
- new_kwargs[name] = arg
- return (
- tuple(args[len(argspec.args):]),
- new_kwargs,
- )
- def extract_all_lambdas(tree):
- lambdas = []
- class Visitor(ast.NodeVisitor):
- def visit_Lambda(self, node):
- lambdas.append(node)
- Visitor().visit(tree)
- return lambdas
- def args_for_lambda_ast(l):
- return [getattr(n, ARG_NAME_ATTRIBUTE) for n in l.args.args]
- LINE_CONTINUATION = re.compile(r"\\\n")
- WHITESPACE = re.compile(r"\s+")
- PROBABLY_A_COMMENT = re.compile("""#[^'"]*$""")
- SPACE_FOLLOWS_OPEN_BRACKET = re.compile(r"\( ")
- SPACE_PRECEDES_CLOSE_BRACKET = re.compile(r" \)")
- def extract_lambda_source(f):
- """Extracts a single lambda expression from the string source. Returns a
- string indicating an unknown body if it gets confused in any way.
- This is not a good function and I am sorry for it. Forgive me my
- sins, oh lord
- """
- argspec = getfullargspec(f)
- arg_strings = []
- # In Python 2 you can have destructuring arguments to functions. This
- # results in an argspec with non-string values. I'm not very interested in
- # handling these properly, but it's important to not crash on them.
- bad_lambda = False
- for a in argspec.args:
- if isinstance(a, (tuple, list)): # pragma: no cover
- arg_strings.append('(%s)' % (', '.join(a),))
- bad_lambda = True
- else:
- assert isinstance(a, str)
- arg_strings.append(a)
- if argspec.varargs:
- arg_strings.append('*' + argspec.varargs)
- elif argspec.kwonlyargs:
- arg_strings.append('*')
- for a in (argspec.kwonlyargs or []):
- default = (argspec.kwonlydefaults or {}).get(a)
- if default:
- arg_strings.append('{}={}'.format(a, default))
- else:
- arg_strings.append(a)
- if_confused = 'lambda %s: <unknown>' % (', '.join(arg_strings),)
- if bad_lambda: # pragma: no cover
- return if_confused
- try:
- source = inspect.getsource(f)
- except IOError:
- return if_confused
- source = LINE_CONTINUATION.sub(' ', source)
- source = WHITESPACE.sub(' ', source)
- source = source.strip()
- assert 'lambda' in source
- tree = None
- try:
- tree = ast.parse(source)
- except SyntaxError:
- for i in hrange(len(source) - 1, len('lambda'), -1):
- prefix = source[:i]
- if 'lambda' not in prefix:
- break
- try:
- tree = ast.parse(prefix)
- source = prefix
- break
- except SyntaxError:
- continue
- if tree is None:
- if source.startswith('@'):
- # This will always eventually find a valid expression because
- # the decorator must be a valid Python function call, so will
- # eventually be syntactically valid and break out of the loop. Thus
- # this loop can never terminate normally, so a no branch pragma is
- # appropriate.
- for i in hrange(len(source) + 1): # pragma: no branch
- p = source[1:i]
- if 'lambda' in p:
- try:
- tree = ast.parse(p)
- source = p
- break
- except SyntaxError:
- pass
- if tree is None:
- return if_confused
- all_lambdas = extract_all_lambdas(tree)
- aligned_lambdas = [
- l for l in all_lambdas
- if args_for_lambda_ast(l) == argspec.args
- ]
- if len(aligned_lambdas) != 1:
- return if_confused
- lambda_ast = aligned_lambdas[0]
- assert lambda_ast.lineno == 1
- source = source[lambda_ast.col_offset:].strip()
- source = source[source.index('lambda'):]
- for i in hrange(len(source), len('lambda'), -1): # pragma: no branch
- try:
- parsed = ast.parse(source[:i])
- assert len(parsed.body) == 1
- assert parsed.body
- if isinstance(parsed.body[0].value, ast.Lambda):
- source = source[:i]
- break
- except SyntaxError:
- pass
- lines = source.split('\n')
- lines = [PROBABLY_A_COMMENT.sub('', l) for l in lines]
- source = '\n'.join(lines)
- source = WHITESPACE.sub(' ', source)
- source = SPACE_FOLLOWS_OPEN_BRACKET.sub('(', source)
- source = SPACE_PRECEDES_CLOSE_BRACKET.sub(')', source)
- source = source.strip()
- return source
- def get_pretty_function_description(f):
- if not hasattr(f, '__name__'):
- return repr(f)
- name = f.__name__
- if name == '<lambda>':
- result = extract_lambda_source(f)
- return result
- elif isinstance(f, types.MethodType):
- self = f.__self__
- if not (self is None or inspect.isclass(self)):
- return '%r.%s' % (self, name)
- return name
- def nicerepr(v):
- if inspect.isfunction(v):
- return get_pretty_function_description(v)
- elif isinstance(v, type):
- return v.__name__
- else:
- return to_str(pretty(v))
- def arg_string(f, args, kwargs, reorder=True):
- if reorder:
- args, kwargs = convert_positional_arguments(f, args, kwargs)
- argspec = getfullargspec(f)
- bits = []
- for a in argspec.args:
- if a in kwargs:
- bits.append('%s=%s' % (a, nicerepr(kwargs.pop(a))))
- if kwargs:
- for a in sorted(kwargs):
- bits.append('%s=%s' % (a, nicerepr(kwargs[a])))
- return ', '.join([nicerepr(x) for x in args] + bits)
- def unbind_method(f):
- """Take something that might be a method or a function and return the
- underlying function."""
- return getattr(f, 'im_func', getattr(f, '__func__', f))
- def check_valid_identifier(identifier):
- if not isidentifier(identifier):
- raise ValueError('%r is not a valid python identifier' %
- (identifier,))
- def eval_directory():
- return storage_directory('eval_source')
- eval_cache = {}
- def source_exec_as_module(source):
- try:
- return eval_cache[source]
- except KeyError:
- pass
- result = ModuleType('hypothesis_temporary_module_%s' % (
- hashlib.sha1(str_to_bytes(source)).hexdigest(),
- ))
- assert isinstance(source, str)
- exec(source, result.__dict__)
- eval_cache[source] = result
- return result
- COPY_ARGSPEC_SCRIPT = """
- from hypothesis.utils.conventions import not_set
- def accept(%(funcname)s):
- def %(name)s(%(argspec)s):
- return %(funcname)s(%(invocation)s)
- return %(name)s
- """.strip() + '\n'
- def define_function_signature(name, docstring, argspec):
- """A decorator which sets the name, argspec and docstring of the function
- passed into it."""
- check_valid_identifier(name)
- for a in argspec.args:
- check_valid_identifier(a)
- if argspec.varargs is not None:
- check_valid_identifier(argspec.varargs)
- if argspec.varkw is not None:
- check_valid_identifier(argspec.varkw)
- n_defaults = len(argspec.defaults or ())
- if n_defaults:
- parts = []
- for a in argspec.args[:-n_defaults]:
- parts.append(a)
- for a in argspec.args[-n_defaults:]:
- parts.append('%s=not_set' % (a,))
- else:
- parts = list(argspec.args)
- used_names = list(argspec.args) + list(argspec.kwonlyargs)
- used_names.append(name)
- for a in argspec.kwonlyargs:
- check_valid_identifier(a)
- def accept(f):
- fargspec = getfullargspec(f)
- must_pass_as_kwargs = []
- invocation_parts = []
- for a in argspec.args:
- if a not in fargspec.args and not fargspec.varargs:
- must_pass_as_kwargs.append(a)
- else:
- invocation_parts.append(a)
- if argspec.varargs:
- used_names.append(argspec.varargs)
- parts.append('*' + argspec.varargs)
- invocation_parts.append('*' + argspec.varargs)
- elif argspec.kwonlyargs:
- parts.append('*')
- for k in must_pass_as_kwargs:
- invocation_parts.append('%(k)s=%(k)s' % {'k': k})
- for k in argspec.kwonlyargs:
- invocation_parts.append('%(k)s=%(k)s' % {'k': k})
- if k in (argspec.kwonlydefaults or []):
- parts.append('%(k)s=not_set' % {'k': k})
- else:
- parts.append(k)
- if argspec.varkw:
- used_names.append(argspec.varkw)
- parts.append('**' + argspec.varkw)
- invocation_parts.append('**' + argspec.varkw)
- candidate_names = ['f'] + [
- 'f_%d' % (i,) for i in hrange(1, len(used_names) + 2)
- ]
- for funcname in candidate_names: # pragma: no branch
- if funcname not in used_names:
- break
- base_accept = source_exec_as_module(
- COPY_ARGSPEC_SCRIPT % {
- 'name': name,
- 'funcname': funcname,
- 'argspec': ', '.join(parts),
- 'invocation': ', '.join(invocation_parts)
- }).accept
- result = base_accept(f)
- result.__doc__ = docstring
- result.__defaults__ = argspec.defaults
- if argspec.kwonlydefaults:
- result.__kwdefaults__ = argspec.kwonlydefaults
- if argspec.annotations:
- result.__annotations__ = argspec.annotations
- return result
- return accept
- def impersonate(target):
- """Decorator to update the attributes of a function so that to external
- introspectors it will appear to be the target function.
- Note that this updates the function in place, it doesn't return a
- new one.
- """
- def accept(f):
- f.__code__ = update_code_location(
- f.__code__,
- target.__code__.co_filename, target.__code__.co_firstlineno
- )
- f.__name__ = target.__name__
- f.__module__ = target.__module__
- f.__doc__ = target.__doc__
- return f
- return accept
- def proxies(target):
- def accept(proxy):
- return impersonate(target)(wraps(target)(define_function_signature(
- target.__name__, target.__doc__, getfullargspec(target))(proxy)))
- return accept
|