24

I'm trying to make my WSGI server implementation compatible with both Python 2 and Python 3. I had this code:

def start_response(status, response_headers, exc_info = None):
    if exc_info:
        try:
            if headers_sent:
                # Re-raise original exception if headers sent.
                raise exc_info[0], exc_info[1], exc_info[2]
        finally:
            # Avoid dangling circular ref.
            exc_info = None
    elif headers_set:
        raise AssertionError("Headers already set!")

    headers_set[:] = [status, response_headers]
    return write

...with the relevant part being:

# Re-raise original exception if headers sent.
raise exc_info[0], exc_info[1], exc_info[2]

Python 3 doesn't support that syntax anymore so it must be translated to:

raise exc_info[0].with_traceback(exc_info[1], exc_info[2])

Problem: the Python 2 syntax generates a parse error in Python 3. How do I write code that can be parsed by both Python 2 and Python 3? I've tried the following, but that doesn't work:

if sys.version_info[0] >= 3:
    raise exc_info[0].with_traceback(exc_info[1], exc_info[2])
else:
    eval("raise exc_info[0], exc_info[1], exc_info[2]; 1", None, { 'exc_info': exc_info })
Hongli
  • 18,682
  • 15
  • 79
  • 107
  • That does not make much sense to me.. Can you put your `start_response` function in some context, maybe show an example where you would call it? – poke Jan 24 '13 at 14:38
  • whats wrong with the classic `try/except`? – Inbar Rose Jan 24 '13 at 14:39
  • 2
    @poke and @inbar rose: `start_response` is part of the WSGI spec. See PEP-333. WSGI apps call start_response when they, well, want to start a response. If `exc_info` is given then that is the WSGI app's signal that the app code encountered an exception, and the WSGI server should do something with it such as printing an error. In my case I want to raise the error if headers have already been sent out. See http://www.python.org/dev/peps/pep-0333/#the-start-response-callable – Hongli Jan 24 '13 at 14:49
  • Quick clarification, you state that the Python 3 syntax generates a parse error in python 2 -- It seems to me that it should be the other way around. The python 2 syntax generates a parse error in python 3 ... – mgilson Jan 24 '13 at 14:54
  • @mgilson Yes that's what I mean. Fixed. – Hongli Jan 24 '13 at 14:56
  • 3
    Have seen this? http://docs.python.org/3/howto/pyporting.html#use-same-source – guettli Jan 24 '13 at 15:47
  • Attention: Answer with the most upvotes comes last: `six.reraise()` – guettli Jun 25 '14 at 09:51
  • // , Is there a way to do this without checking for the version or using `six`? I asked about it here: http://stackoverflow.com/questions/32319317/without-version-checking-or-six-how-can-i-use-except-myerror-e-vs-e – Nathan Basanese Aug 31 '15 at 20:20

2 Answers2

50

Can you use six? It exists to solve this very problem.

import six, sys
six.reraise(*sys.exc_info())

See: https://six.readthedocs.io/index.html#six.reraise

Jon-Eric
  • 16,977
  • 9
  • 65
  • 97
  • It worth pointing out that `six.reraise` behaves differently for different platforms. See: https://github.com/benjaminp/six/issues/242 – Godfrey Jun 03 '20 at 10:23
0

You could do something creative.

Have a check at the start of your code - your constructor or whatever, check what version of python you are using, since your normal version checker is not working, try this instead:

try:
  eval('a python 3 expression') # something that only works in python3+
  python_version = 3
except:
  python_version = 2

Then the rest of your code can easily just reference this to know what to use.

As for the parse errors, you can use exec in a function, like so:

def what_to_run():
    if python_version = 3:
        return 'raise exc_info[0].with_traceback(exc_info[1], exc_info[2])'
    else:
        return 'raise exc_info[0], exc_info[1], exc_info[2]'

In your function you would write this:

def start_response(status, response_headers, exc_info = None):
    if exc_info:
        try:
            if headers_sent:
                # Re-raise original exception if headers sent.
                exec(what_to_run())
        finally:
            # Avoid dangling circular ref.
            exc_info = None
    elif headers_set:
        raise AssertionError("Headers already set!")

    headers_set[:] = [status, response_headers]
    return write

A bit messy, untested, but it should work, at least you understand the idea.

Inbar Rose
  • 41,843
  • 24
  • 85
  • 131
  • There's a simpler way: `PY3K = sys.version_info >= (3,)` (used by `six`, among others). –  Jan 24 '13 at 14:44
  • i would normally agree with you, but OP said that he tried `if sys.version_info[0] >= 3:` and it didn't work... – Inbar Rose Jan 24 '13 at 14:45
  • 5
    Checking for Python is not the problem. The problem is that Python 3-compatible code generates a *parse error* on Python 2 and vice versa. I can't just put both versions in an if-else or try-except block like I usually do. – Hongli Jan 24 '13 at 14:46
  • Your edit doesn't work. `eval('raise exc_info[0], exc_info[1], exc_info[2]')` is exactly what I've tried (as explained in the question) and it raises a syntax error. – Hongli Jan 24 '13 at 14:52
  • 1
    @Hongli -- I'm pretty sure `eval` doesn't work since `raise` is a *statement*, not an expression. If you changed it to `exec` it might work though... – mgilson Jan 24 '13 at 14:55
  • @Hongli also - sorry, i had the wrong if condition, now it should return the right eval expression, it was returning the p2 for p3 and p3 for p2 by mistake. – Inbar Rose Jan 24 '13 at 14:57
  • 1
    @InbarRose -- But there's still the problem with using `eval` on a statement when `eval` requires an *expression* – mgilson Jan 24 '13 at 14:58
  • @mgilson yes, fixed it too. thanks. :) its working for you now? thats great! – Inbar Rose Jan 24 '13 at 15:01
  • Yes it works. If you're interested, this is the full code: https://github.com/FooBarWidget/passenger/blob/master/helper-scripts/wsgi-loader.py – Hongli Jan 24 '13 at 17:21
  • 7
    Don't do this. Use `six` instead, as the other answer suggests. – fletom Mar 04 '13 at 06:11
  • // , Is there a way to do this without `exec()` or `six`? – Nathan Basanese Aug 31 '15 at 20:21
  • // , Asked about it here: http://stackoverflow.com/questions/32319317/without-version-checking-or-six-how-can-i-use-except-myerror-e-vs-e – Nathan Basanese Aug 31 '15 at 20:21