Repository: HypothesisWorks/hypothesis

Issue:
Regression in lambda inspection
When updating the statistics output in documentation for #978 I noticed various strategies' repr no longer included the complete lambda used in defining them. e.g. the following example:

```python
from hypothesis import given, event, strategies as st

@given(st.integers().filter(lambda x: x % 2 == 0))
def test_even_integers(i):
    pass
```

When printing statistics by running this under pytest with `--hypothesis-show-statistics` the lambda used to be correctly displayed and now shows up as `lambda x: <unknown>`


Discussion:
Yeah, this would have regressed in Hypothesis 3.6.0 when we removed `uncompyle6`. From the changelog:

> This release reverts Hypothesis to its old pretty printing of lambda functions based on attempting to extract the source code rather than decompile the bytecode. This is unfortunately slightly inferior in some cases and may result in you occasionally seeing things like `lambda x: <unknown>` in statistics reports and strategy reprs.

The `--hypothesis-show-statistics` flag was introduced in 3.5.0, and then the Lambda printing was broken almost immediately afterward.
Ah, good catch. I hadn't thought we'd released any interesting features between adding and removing the decompiler, but apparently I was misremembering!

I think this can probably be fixed without adding  a decompiler back in by just extending the existing hacky solution.
> the existing hacky solution.

TFW you'd forgotten just how bad this code past-you wrote was. 😓 

Resolving patch:
diff --git a/src/hypothesis/internal/reflection.py b/src/hypothesis/internal/reflection.py
--- a/src/hypothesis/internal/reflection.py
+++ b/src/hypothesis/internal/reflection.py
@@ -282,6 +282,9 @@ def extract_lambda_source(f):
     source = LINE_CONTINUATION.sub(' ', source)
     source = WHITESPACE.sub(' ', source)
     source = source.strip()
+    assert 'lambda' in source
+
+    tree = None
 
     try:
         tree = ast.parse(source)
@@ -289,15 +292,32 @@ def extract_lambda_source(f):
         for i in hrange(len(source) - 1, len('lambda'), -1):
             prefix = source[:i]
             if 'lambda' not in prefix:
-                return if_confused
+                break
             try:
                 tree = ast.parse(prefix)
                 source = prefix
                 break
             except SyntaxError:
                 continue
-        else:
-            return if_confused
+    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 = [


Test patch:
diff --git a/tests/cover/test_lambda_formatting.py b/tests/cover/test_lambda_formatting.py
--- a/tests/cover/test_lambda_formatting.py
+++ b/tests/cover/test_lambda_formatting.py
@@ -42,6 +42,7 @@ def test_can_get_descriptions_of_nested_lambdas_with_different_names():
     assert get_pretty_function_description(ordered_pair) == \
         'lambda right: [].map(lambda length: ())'
 
+
 def test_does_not_error_on_unparsable_source():
     t = [
         lambda x: \
@@ -50,6 +51,7 @@ def test_does_not_error_on_unparsable_source():
         x][0]
     assert get_pretty_function_description(t) == 'lambda x: <unknown>'
 
+
 def test_source_of_lambda_is_pretty():
     assert get_pretty_function_description(
         lambda x: True
@@ -115,3 +117,64 @@ def test_lambda_source_break_after_def_with_line_continuation():
 
     source = get_pretty_function_description(f)
     assert source == "lambda n: 'aaa'"
+
+
+def arg_decorator(*s):
+    def accept(f):
+        return s
+    return accept
+
+
+@arg_decorator(lambda x: x + 1)
+def plus_one():
+    pass
+
+
+@arg_decorator(lambda x: x + 1, lambda y: y * 2)
+def two_decorators():
+    pass
+
+
+def test_can_extract_lambda_repr_in_a_decorator():
+    assert get_pretty_function_description(plus_one[0]) == 'lambda x: x + 1'
+
+
+def test_can_extract_two_lambdas_from_a_decorator_if_args_differ():
+    a, b = two_decorators
+    assert get_pretty_function_description(a) == 'lambda x: x + 1'
+    assert get_pretty_function_description(b) == 'lambda y: y * 2'
+
+
+@arg_decorator(
+    lambda x: x + 1
+)
+def decorator_with_space():
+    pass
+
+
+def test_can_extract_lambda_repr_in_a_decorator_with_spaces():
+    assert get_pretty_function_description(
+        decorator_with_space[0]) == 'lambda x: x + 1'
+
+
+@arg_decorator(lambda: ())
+def to_brackets():
+    pass
+
+
+def test_can_handle_brackets_in_decorator_argument():
+    assert get_pretty_function_description(to_brackets[0]) == "lambda: ()"
+
+
+def identity(x):
+    return x
+
+
+@arg_decorator(identity(lambda x: x + 1))
+def decorator_with_wrapper():
+    pass
+
+
+def test_can_handle_nested_lambda_in_decorator_argument():
+    assert get_pretty_function_description(
+        decorator_with_wrapper[0]) == "lambda x: x + 1"
diff --git a/tests/cover/test_statistical_events.py b/tests/cover/test_statistical_events.py
--- a/tests/cover/test_statistical_events.py
+++ b/tests/cover/test_statistical_events.py
@@ -158,3 +158,14 @@ def test(_):
         assert stats.draw_time_percentage == '~ 50%'
     else:
         assert stats.draw_time_percentage == '~ 100%'
+
+
+def test_has_lambdas_in_output():
+    @given(st.integers().filter(lambda x: x % 2 == 0))
+    def test(i):
+        pass
+
+    stats = call_for_statistics(test)
+    assert any(
+        'lambda x: x % 2 == 0' in e for e in stats.events
+    )