How to write exception reraising code that's compatible with both Python 2 and Python 3?
Asked Answered
F

2

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 })
Flyblown answered 24/1, 2013 at 14:33 Comment(8)
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?Evalynevan
whats wrong with the classic try/except?Imagery
@Evalynevan 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 python.org/dev/peps/pep-0333/#the-start-response-callableFlyblown
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 ...Swift
@Swift Yes that's what I mean. Fixed.Flyblown
Have seen this? docs.python.org/3/howto/pyporting.html#use-same-sourceDanczyk
Attention: Answer with the most upvotes comes last: six.reraise()Danczyk
// , Is there a way to do this without checking for the version or using six? I asked about it here: #32319817Similar
I
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.

Imagery answered 24/1, 2013 at 14:42 Comment(12)
There's a simpler way: PY3K = sys.version_info >= (3,) (used by six, among others).Basilius
i would normally agree with you, but OP said that he tried if sys.version_info[0] >= 3: and it didn't work...Imagery
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.Flyblown
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.Flyblown
@Flyblown -- 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...Swift
@Flyblown 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.Imagery
@InbarRose -- But there's still the problem with using eval on a statement when eval requires an expressionSwift
@Swift yes, fixed it too. thanks. :) its working for you now? thats great!Imagery
Yes it works. If you're interested, this is the full code: github.com/FooBarWidget/passenger/blob/master/helper-scripts/…Flyblown
Don't do this. Use six instead, as the other answer suggests.Kelcie
// , Is there a way to do this without exec() or six?Similar
// , Asked about it here: #32319817Similar
C
51

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

Crucify answered 24/1, 2013 at 15:38 Comment(1)
It worth pointing out that six.reraise behaves differently for different platforms. See: github.com/benjaminp/six/issues/242Snore
I
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.

Imagery answered 24/1, 2013 at 14:42 Comment(12)
There's a simpler way: PY3K = sys.version_info >= (3,) (used by six, among others).Basilius
i would normally agree with you, but OP said that he tried if sys.version_info[0] >= 3: and it didn't work...Imagery
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.Flyblown
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.Flyblown
@Flyblown -- 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...Swift
@Flyblown 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.Imagery
@InbarRose -- But there's still the problem with using eval on a statement when eval requires an expressionSwift
@Swift yes, fixed it too. thanks. :) its working for you now? thats great!Imagery
Yes it works. If you're interested, this is the full code: github.com/FooBarWidget/passenger/blob/master/helper-scripts/…Flyblown
Don't do this. Use six instead, as the other answer suggests.Kelcie
// , Is there a way to do this without exec() or six?Similar
// , Asked about it here: #32319817Similar

© 2022 - 2024 — McMap. All rights reserved.