5

I have a scenario where I'm dynamically running functions at run-time and need to keep track of a "localized" scope. In the example below, "startScope" and "endScope" would actually be creating levels of "nesting" (in reality, the stuff contained in this localized scope isn't print statements...it's function calls that send data elsewhere and the nesting is tracked there. startScope / endScope just set control flags that are used to start / end the current nesting depth).

This all works fine for tracking the nested data, however, exceptions are another matter. Ideally, an exception would result in "falling out" of the current localized scope and not end the entire function (myFunction in the example below).

def startScope():
    #Increment our control object's (not included in this example) nesting depth
    control.incrementNestingDepth()

def endScope():
    #Decrement our control object's (not included in this example) nesting depth
    control.decrementNestingDepth()

def myFunction():
    print "A"
    print "B"

    startScope()
    print "C"
    raise Exception
    print "D"
    print "This print statement and the previous one won't get printed"
    endScope()

    print "E"

def main():
    try:
        myFunction()
    except:
        print "Error!"

Running this would (theoretically) output the following:

>>> main()
A
B
C
Error!
E

>>>

I'm quite certain this isn't possible as I've written it above - I just wanted to paint a picture of the sort of end-result I'm trying to achieve.

Is something like this possible in Python?

Edit: A more relevant (albeit lengthy) example of how this is actually being used:

class Log(object):
    """
    Log class
    """

    def __init__(self):
        #DataModel is defined elsewhere and contains a bunch of data structures / handles nested data / etc...
        self.model = DataModel()

    def Warning(self, text):
        self.model.put("warning", text)

    def ToDo(self, text):
        self.model.put("todo", text)

    def Info(self, text):
        self.model.put("info", text)

    def StartAdvanced(self):
        self.model.put("startadvanced")

    def EndAdvanced(self):
        self.model.put("endadvanced")

    def AddDataPoint(self, data):
        self.model.put("data", data)

    def StartTest(self):
        self.model.put("starttest")

    def EndTest(self):
        self.model.put("endtest")

    def Error(self, text):
        self.model.put("error", text)


#myScript.py

from Logger import Log

def test_alpha():
    """
    Crazy contrived example

    In this example, there are 2 levels of nesting...everything up to StartAdvanced(),
    and after EndAdvanced() is included in the top level...everything between the two is
    contained in a separate level.
    """

    Log.Warning("Better be careful here!")
    Log.AddDataPoint(fancyMath()[0])

    data = getSerialData()

    if data:
        Log.Info("Got data, let's continue with an advanced test...")

        Log.StartAdvanced()

        #NOTE: If something breaks in one of the following methods, then GOTO (***)
        operateOnData(data)
        doSomethingCrazy(data)
        Log.ToDo("Fill in some more stuff here later...")
        Log.AddDataPoint(data)

        Log.EndAdvanced()

    #(***) Ideally, we would resume here if an exception is raised in the above localized scope
    Log.Info("All done!  Log some data and wrap everything up!")
    Log.AddDataPoint({"data": "blah"})

    #Done


#framework.py

import inspect
from Logger import Log

class Framework(object):

    def __init__(self):
        print "Framework init!"
        self.tests = []

    def loadTests(self, file):
        """
        Simplifying this for the sake of clarity
        """

        for test in file:
            self.tests.append(test)

    def runTests(self):
        """
        Simplifying this for the sake of clarity
        """

        #test_alpha() as well as any other user tests will be run here
        for test in self.tests:
            Log.StartTest()

            try:
                test()
            except Exception,e :
                Log.Error(str(e))

            Log.EndTest()

#End
2
  • Can you clarify which aspects of this are must-haves and which are flexible? Also, do you need/want these "scopes" to have any impact on anything other than exception handling? If the exception is caught in main, there is no way you can get print "E" to execute; once the exception propagates up, you can't "resume" execution at a lower level. Commented Sep 30, 2015 at 6:01
  • For the sake of this example, I'm only concerned with the result. The example (as written above) clearly won't work for the reason you just mentioned. As for flexibility - I don't have direct control over the contents of "myFunction"...all I know is that "startScope" and "endScope" will be called, and I can handle those calls in whatever way I see fit. Currently, this means pushing / popping from a stack in a separate control class. This works for data, but exception handling is another story. My hope is that there's some sort of common design pattern that deals with this sort of thing... Commented Sep 30, 2015 at 6:03

1 Answer 1

3

You can achieve a similar effect with a context manager using a with statement. Here I use the contextlib.contextmanager decorator:

@contextlib.contextmanager
def swallower():
    try:
        yield
    except ZeroDivisionError:
        print("We stopped zero division error")

def foo():
    print("This error will be trapped")
    with swallower():
        print("Here comes error")
        1/0
        print("This will never be reached")
    print("Merrily on our way")
    with swallower():
        print("This error will propagate")
        nonexistentName
    print("This won't be reached")

>>> foo()
This error will be trapped
Here comes error
We stopped zero division error
Merrily on our way
This error will propagate
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    foo()
  File "<pyshell#3>", line 10, in foo
    nonexistentName
NameError: global name 'nonexistentName' is not defined

It cannot be done with an ordinary function call as in your example. In your example, the function startScope returns before the rest of the body of myFunction executes, so startScope can't have any effect on it. To handle exceptions, you need some kind of explicit structure (either a with statement or a regular try/except) inside myFunction; there's no way to make a simple function call magically intercept exceptions that are raised in its caller.

You should read up on context managers as they seem to fit what you're trying to do. The __enter__ and __exit__ methods of the context manager would correspond to your startScope and endScope. Whether it will do exactly what you want depends on exactly what you want those "manager" functions to do, but you will probably have more luck doing it with a context manager than trying to do it with simple function calls.

Sign up to request clarification or add additional context in comments.

7 Comments

This is probably about as close as I'm going to get to the desired result I think...Unfortunately, I don't have direct control over foo() as in your example above. "foo" would be written by someone else, and the goal is to be able to give them access to some magic methods that would handle the context for them. So they would simply call "startMagic()", "errorFunc()", and then "endMagic()". My goal is to make sure that when stuff breaks, we fall down to "endMagic" and skip whatever else was contained in "errorFunc" after the exception was raised.
@Novark: You don't necessarily need direct control over foo. If you're writing an API for writers of foo to use, I think you can likely make it work. What you should do is give them access to the context manager, so that instead of startMagic()...endMagic() they do with magicManager: .... I'd encourage you to experiment with a context manager solution. What type of case do you think it won't handle?
I've included an extended example in my original post that's a bit more relevant to what I'm working with...This example shows how things currently work, however, it has the problem of falling all the way out of the test_alpha() function when an error is encountered, rather than to the end of the local scope (hopefully that makes sense - let me know if I can clarify anything)
@Novark: As far as I can tell, my example will handle that example fine, using a context manager for the "advanced" block. If your question is "can I make this existing API with function calls handle exceptions this way", then the answer is no. If your question is can I make an API that handles exceptions this way, the answer is (at least potentially) yes. But you need to change your API to use context managers instead of plain function calls.
Got it. I'll take a closer look at context managers and see if they'll fit the bill. For my particular use-case, it's not super critical that the exceptions resume at the next-nested-level, however, it would be a nice feature to have. I'll have to weigh the options and see which one works best. Thanks for the help!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.