views:

225

answers:

5

Can you suggest a way to code a drop-in replacement for the "with" statement that will work in Python 2.4?

It would be a hack, but it would allow me to port my project to Python 2.4 more nicely.

EDIT: Removed irrelevant metaclass sketch

+4  A: 

You could (ab)use decorators to do this, I think. The following works, eg:

def execute_with_context_manager(man):
    def decorator(f):
        target = man.__enter__()
        exc = True
        try:
            try:
                f(target)
            except:
                exc = False
                if not man.__exit__(*sys.exc_info()):
                    raise
        finally:
            if exc:
                man.__exit__(None, None, None)
        return None
    return decorator

@execute_with_context_manager(open("/etc/motd"))
def inside(motd_file):
    for line in motd_file:
        print line,

(Well, in Python 2.4 file objects don't have __enter__ and __exit__ methods, but otherwise it works)

The idea is you're replacing the with line in:

with bar() as foo:
    do_something_with(foo)
    do_something_else_with(foo)
    # etc...

with the decorated function "declaration" in:

@execute_with_context_manager( bar() )
def dummyname( foo ):
    do_something_with(foo)
    do_something_else_with(foo)
    # etc...

but getting the same behaviour (the do_something_... code executed). Note the decorator changes the function declaration into an immediate invocation which is more than a little evil.

Anthony Towns
That's not really a drop in replacement, because this assumes that the code is a function, while it might be just a few lines of code. And having to wrap it in a function would be too annoying.
cool-RR
isn't it __with expression [as varname]:__
Shrikant Sharat
@cool-RR the function wrapping is just syntax to separate out the code inside the "with" from the code outside it, like the class in the question.
Anthony Towns
Your code could be partially fixed by actually carrying out the function in the decorator. That would make it a bit more similar to the structure of "with", except that yours has one extra line, the @ line. I guess if no better solution presents itself I will take that.
cool-RR
@cool-RR the decorator does invoke the function it's decorating -- that's the f(man) line
Anthony Towns
You're invoking it inside the decorated function, but not in the decorator itself. This way you would have to actually call the function after you define it, which is unwanted. Better have the decorator just call the function itself.
cool-RR
@cool-RR @foo(bar) / def baz(...): -- invokes baz = (foo(bar))(baz) -- so you have to return a function which is then immediately invoked.
Anthony Towns
If you don't want to make the block into a function, I think a try/except/finally is the best solution. Don't see what's so horrid with that solution that you want to fake a context manager anyway. :)
Lennart Regebro
Okay Anthony, I see my error. Now my only question is, why did you modify the code from the PEP?
cool-RR
+1  A: 

Since you need to exit the context manager both during errors and not errors, I don't think it's possible to do a generic usecase with metaclasses, or in fact at all. You are going to need try/finally blocks for that.

But maybe it's possible to do something else in your case. That depends on what you use the context manager for.

Using __del__ can help in some cases, like deallocating resource, but since you can't be sure it gets called, it can only be used of you need to release resources that will be released when the program exits. That also won't work if you are handling exceptions in the __exit__ method.

I guess the cleanest method is to wrap the whole context management in a sort of context managing call, and extract the code block into a method. Something like this (untested code, but mostly stolen from PEP 343):

def call_as_context_manager(mgr, function):
    exit = mgr.__exit__
    value = mgr.__enter__()
    exc = True
    try:
        try:
            function(value)
        except:
            exc = False
            if not exit(*sys.exc_info()):
                raise
    finally:
        if exc:
            exit(None, None, None)
Lennart Regebro
+3  A: 

Just use try-finally.

Really, this may be nice as a mental exercise, but if you actually do it in code you care about you will end up with ugly, hard to maintain code.

itsadok
+1  A: 

How about this?

def improvize_context_manager(*args, **kwargs):

    assert (len(args) + len(kwargs)) == 1
    if args:
        context_manager = args[0]
        as_ = None
    else: # It's in kwargs
        (as_, context_manager) = kwargs.items()[0]

    def decorator(f):
        exit_ = context_manager.__exit__  # Not calling it yet
        enter_ = context_manager.__enter__()
        exc = True
        try:
            try:
                if as_:
                    f(*{as_: enter_})
                else:
                    f()
            except:
                exc = False
                if not exit_(*sys.exc_info()):
                    raise
        finally:
            if exc:
            exit_(None, None, None)
        return None
    return decorator

Usage:

@improvize_context_manager(lock)
def null():
    do(stuff)

Which parallels the with keyword without as.

Or:

@improvize_context_manager(my_lock=lock)
def null(my_lock):
    do(stuff_with, my_lock)

Which parallels the with keyword with the as.

cool-RR
I just realize it won't work, nor will Anthony's suggestion. Because if you try to return something inside the "function", you would not really return in the outer block.
cool-RR
That's actually OK, because this is not a real function. You're just using the function as a syntactical device. Nobody should ever call it anyway. In fact, I would do `del null` right after defining it, just to make sure.
itsadok
A: 

If you are OK with using def just to get a block, and decorators that immediately execute, you could use the function signature to get something more natural for the named case.

import sys
def with(func):
    def decorated(body = func):
        contexts = body.func_defaults
        try:
            exc = None, None, None
            try:
                for context in contexts:
                    context.__enter__()
                body()
            except:
                exc = sys.exc_info()
                raise
        finally:
            for context in reversed(contexts):
                context.__exit__(*exc)
    decorated()

class Context(object):
    def __enter__(self):
        print "Enter %s" % self
    def __exit__(self, *args):
        print "Exit %s(%s)" % (self, args)

x = Context()

@with
def _(it = x):
    print "Body %s" % it

@with
def _(it = x):
    print "Body before %s" % it
    raise "Nothing"
    print "Body after %s" % it

Jon Jay Obermark