Repository: HypothesisWorks/hypothesis

Issue:
SyntaxError on Python 2 with 3.79.2
The latest release causes a SyntaxError on older Python 2.7 (occurs on 2.7.3 and 2.7.5 in pyca/cryptography's CI):

```python-traceback
Traceback (most recent call last):
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pytest.py", line 77, in <module>
    raise SystemExit(pytest.main())
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/_pytest/config/__init__.py", line 49, in main
    config = _prepareconfig(args, plugins)
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/_pytest/config/__init__.py", line 186, in _prepareconfig
    pluginmanager=pluginmanager, args=args
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pluggy/hooks.py", line 284, in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pluggy/manager.py", line 67, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pluggy/manager.py", line 61, in <lambda>
    firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pluggy/callers.py", line 203, in _multicall
    gen.send(outcome)
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/_pytest/helpconfig.py", line 89, in pytest_cmdline_parse
    config = outcome.get_result()
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pluggy/callers.py", line 81, in get_result
    _reraise(*ex)  # noqa
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pluggy/callers.py", line 187, in _multicall
    res = hook_impl.function(*args)
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/_pytest/config/__init__.py", line 656, in pytest_cmdline_parse
    self.parse(args)
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/_pytest/config/__init__.py", line 828, in parse
    self._preparse(args, addopts=addopts)
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/_pytest/config/__init__.py", line 780, in _preparse
    self.pluginmanager.load_setuptools_entrypoints("pytest11")
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pluggy/manager.py", line 267, in load_setuptools_entrypoints
    plugin = ep.load()
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2332, in load
    return self.resolve()
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2338, in resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/hypothesis/__init__.py", line 30, in <module>
    from hypothesis.core import given, find, example, seed, reproduce_failure, \
  File "/home/jenkins/workspace/4485-S2JEK2AOWI4X57D@2/cryptography/.tox/py27/local/lib/python2.7/site-packages/hypothesis/core.py", line 948
    exec(
SyntaxError: unqualified exec is not allowed in function 'wrapped_test' it is a nested function
```


Discussion:
Oh... dear.  I don't want to drop traceback elision entirely for Python 2, but the best solution so far is `eval(exec(...))` behind a version guard 😱... I'll keep looking.
The root cause is [BPO-21591](https://bugs.python.org/issue21591), so I don't think there's going to be a better solution 😭 

Resolving patch:
diff --git a/hypothesis-python/docs/conf.py b/hypothesis-python/docs/conf.py
--- a/hypothesis-python/docs/conf.py
+++ b/hypothesis-python/docs/conf.py
@@ -107,6 +107,7 @@
     'issue': (_repo + 'issues/%s', 'issue #'),
     'pull': (_repo + 'pull/%s', 'pull request #'),
     'pypi': ('https://pypi.org/project/%s', ''),
+    'bpo': ('https://bugs.python.org/issue%s', 'bpo-'),
 }
 
 # -- Options for HTML output ----------------------------------------------
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -944,11 +944,13 @@ def wrapped_test(*arguments, **kwargs):
                     if PY2:
                         # Python 2 doesn't have Exception.with_traceback(...);
                         # instead it has a three-argument form of the `raise`
-                        # statement.  Which is a SyntaxError on Python 3.
-                        exec(
-                            'raise type(e), e, get_trimmed_traceback()',
-                            globals(), locals()
-                        )
+                        # statement.  Unfortunately this is a SyntaxError on
+                        # Python 3, and before Python 2.7.9 it was *also* a
+                        # SyntaxError to use it in a nested function so we
+                        # can't `exec` or `eval` our way out (BPO-21591).
+                        # So unless we break some versions of Python 2, none
+                        # of them get traceback elision.
+                        raise
                     # On Python 3, we swap out the real traceback for our
                     # trimmed version.  Using a variable ensures that the line
                     # which will actually appear in trackbacks is as clear as


Test patch:
diff --git a/hypothesis-python/tests/cover/test_escalation.py b/hypothesis-python/tests/cover/test_escalation.py
--- a/hypothesis-python/tests/cover/test_escalation.py
+++ b/hypothesis-python/tests/cover/test_escalation.py
@@ -17,14 +17,11 @@
 
 from __future__ import division, print_function, absolute_import
 
-import sys
-import traceback
-
 import pytest
 
 import hypothesis.strategies as st
 import hypothesis.internal.escalation as esc
-from hypothesis import Verbosity, given, settings
+from hypothesis import given
 
 
 def test_does_not_escalate_errors_in_non_hypothesis_file():
@@ -69,22 +66,3 @@ def test(i):
         test()
 
     assert count == [1]
-
-
-@pytest.mark.parametrize('verbosity', [Verbosity.normal, Verbosity.debug])
-def test_tracebacks_omit_hypothesis_internals(verbosity):
-    @settings(verbosity=verbosity)
-    @given(st.none())
-    def simplest_failure(x):
-        assert x
-
-    try:
-        simplest_failure()
-    except AssertionError:
-        tb = traceback.extract_tb(sys.exc_info()[2])
-        # Unless in debug mode, Hypothesis adds 1 frame - the least possible!
-        # (4 frames: this one, simplest_failure, internal frame, assert False)
-        if verbosity < Verbosity.debug:
-            assert len(tb) == 4
-        else:
-            assert len(tb) >= 5
diff --git a/hypothesis-python/tests/py3/test_traceback_elision.py b/hypothesis-python/tests/py3/test_traceback_elision.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/py3/test_traceback_elision.py
@@ -0,0 +1,44 @@
+# 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
+
+from __future__ import division, print_function, absolute_import
+
+import traceback
+
+import pytest
+
+import hypothesis.strategies as st
+from hypothesis import Verbosity, given, settings
+
+
+@pytest.mark.parametrize('verbosity', [Verbosity.normal, Verbosity.debug])
+def test_tracebacks_omit_hypothesis_internals(verbosity):
+    @settings(verbosity=verbosity)
+    @given(st.none())
+    def simplest_failure(x):
+        assert x
+
+    try:
+        simplest_failure()
+    except AssertionError as e:
+        tb = traceback.extract_tb(e.__traceback__)
+        # Unless in debug mode, Hypothesis adds 1 frame - the least possible!
+        # (4 frames: this one, simplest_failure, internal frame, assert False)
+        if verbosity < Verbosity.debug:
+            assert len(tb) == 4
+        else:
+            assert len(tb) >= 5