pymxs.undo and raising exceptions

pymxs.undo and raising exceptions

morten_bohne
Advocate Advocate
2,751 Views
7 Replies
Message 1 of 8

pymxs.undo and raising exceptions

morten_bohne
Advocate
Advocate

I'm working in max2017, and I'm having a problem when using pymxs.undo on a function that raises an exception:

import pymxs
from pymxs import runtime as rt

def make_box_and_raise(): with pymxs.undo(True, "Making box!"): rt.Box() raise RuntimeError("Does this get raised?") make_box_and_raise() # I would expect this to raise a RuntimeError, but nothing happens (and the box doesn't get created)

I would expect max to start coughing and raise my RuntimeError, but instead nothing happens. Do people usually just work around this issue (guess i could write my own contextmanager for undo, catching the errors and raising it outside the pymxs.undo context) or is that bug fixed in newer releases? or is undo'ing just one of those things people long ago accepted didn't work in python for max?

0 Likes
Accepted solutions (1)
2,752 Views
7 Replies
Replies (7)
Message 2 of 8

eric.brosseau
Autodesk
Autodesk
Accepted solution

Morten,

Raising an exception within the pymxs.undo block will 'undo' anything that was created prior the raised exception within the block. The exception is not propagated back, as it is considered already handled, and no message is sent back to the user unfortunately. It was coded this way originally; there is an exception handler within the pymxs.undo generator that does the trick.

 

You can see part of the code in the file <maxroot>/pymxsExtend.py.

 

We will update our documentation for the MAXScript Context Expressions.

 

We might look into adding a notification (without raising an exception that was already handled by the undo system) to inform the user something went wrong during execution.

 

Thank you for bringing this up to our attention.

Eric Brosseau
Senior Software Developer, 3ds Max, Autodesk
0 Likes
Message 3 of 8

morten_bohne
Advocate
Advocate

Hey Eric, thanks for your reply. I Think better documentation for undo would be really helpful. Would be nice if it could also clarify what operations are undoable in max (eg: setting userProperties isn't: https://forums.autodesk.com/t5/3ds-max-programming/undo-setuserprop-not-working/m-p/7154021#M16296 )

 

If anyone is interested in a way of bypassing the undo-exception-handler, here is a quick example:

 

import contextlib
import pymxs

@contextlib.contextmanager
def undo(enabled=True, message=""):
"""
Uses pymxs's undo mechanism, but doesn't silence exceptions raised
in it.

:param bool enabled: Turns undo functionality on
:param str message: Label for the undo item in the undo menu
"""
e = None
with pymxs.undo(enabled, message):
try:
yield
except Exception as e:
pass
if e:
pymxs.run_undo()
raise e

 

Message 4 of 8

Anonymous
Not applicable

Came across this a lil bit ago when trying to find out why exceptions were getting eaten. If I use the code above I actually get another error saying that local var e is ref'd before it's called (on the if e line)

 

Using this I find that I get a good error print out as well as it working correctly

 

import contextlib
import traceback

@contextlib.contextmanager
def undo(enabled=True, message=""):
    """
    https://forums.autodesk.com/t5/3ds-max-programming/pymxs-undo-and-raising-exceptions/m-p/8478114/highlight/true#M21929
    Uses pymxs's undo mechanism, but doesn't silence exceptions raised
    in it.

    :param bool enabled: Turns undo functionality on
    :param str message: Label for the undo item in the undo menu
    """
    e = None
    with pymxs.undo(enabled, message):
        try:
            yield
        except Exception as e:
            # print error, raise error then run undo 
            print(traceback.print_exc())
            raise(e)
            pymxs.run_undo()

Have only tested in 2021, but should work in previous versions as well

Message 5 of 8

swchung
Participant
Participant

As @Anonymous mentioned, @morten_bohne 's answer will not work unfortunately. It will raise UnboundLocalError in contextlib.py.

 

However, it can be fixed with minor modification 😉

import contextlib
import pymxs

@contextlib.contextmanager
def undo(enabled=True, message=""):
"""
Uses pymxs's undo mechanism, but doesn't silence exceptions raised
in it.

:param bool enabled: Turns undo functionality on
:param str message: Label for the undo item in the undo menu
"""
exception = None
with pymxs.undo(enabled, message):
try:
yield
except Exception as e:
# Just `pass`ing without explicit assignment will not work as expected with this exception in contextlib.py:
# UnboundLocalError: local variable 'e' referenced before assignment
exception = e
if exception is not None:
pymxs.run_undo()
raise exception

 

This solution is different with @Anonymous 's answer, which prints the traceback but absorbs exceptions rather than propagating.

0 Likes
Message 6 of 8

denisT.MaxDoctor
Advisor
Advisor

"Let's try to do something with undo/redo enabled, and if it fails, we'll play undo back...".

 

From the point of view of system logic, this approach is not correct at all and should not be used in the general case.

 

If something went wrong, the undo record must be canceled.

0 Likes
Message 7 of 8

swchung
Participant
Participant

My point was `UnboundLocalError` could be resolved by explicitly assigning exception `e` to another variable. Everything else was from @morten_bohne 's original code.

 

Indeed, the vanilla `pymxs.undo()` absorbs all the unhandled exceptions in the context and makes up everything is fine. All the actions right before the exception will be recorded in undo stack. Something is recorded to the undo stack but no one knows there was a failure. It makes debugging harder. This is a bad design, IMO.

 

Always calling `pymxs.run_undo()` might be an excessive behavior and someone might want to remove that line. It will be more wise to take over the responsibility of failure handling to the caller, not the context itself (callee didn't know how the handle this and it became "unhandled"). The caller always has a better knowledge of what they doing so they can decide the correct way to react on the failure. To give a chance to the caller to react on the failure, there's need to propagate the exception to the outside of undo context. I think this is the key of the proposed code.

 

About "canceling the incomplete undo record" rather than "undoing using the incomplete record", unfortunately there's no way to accomplish this, AFAIK. Maxscript has no command to "cancel" the single undo record. `clearUndoBuffer()` will flush all the undo records. That's why `pymxs.run_undo()` after the failure of undo context could be considered as second worst solution. This mitigation will left the re-do record instead of un-do and it is distant from "cancel", but at least it prevents hanging in the middle of failure state, which normal users don't expect.

 

---

 


Raising an exception within the pymxs.undo block will 'undo' anything that was created prior the raised exception within the block.

Edit: Oh, I missed this sentence that MAX actually do cancel ;( But propagating exceptions back will be still helpful to the caller to decide post-failure actions.

0 Likes
Message 8 of 8

denisT.MaxDoctor
Advisor
Advisor

I did not comment on this original question from five years ago because the question contains a misunderstanding of how the "undo context" works. And the official answer didn't clear the issue.

 

The "undo context" only allows or disallows "undo" registration during a block of operations, but does not guarantee that this registration will occur. So there is no point in trying to do an "undo" for both a successful and unsuccessful cases, because there may not be any undo at all.

 

By calling "undo" after a failed operation, you can't know for sure that you are actually "undoing" because you can't track "undo" records in a general case. But you can cancel an "undo" recording, as it is only enough to do so if you have announced that you want an undo, and it doesn't depend on whether the undo record was in effect registered or not. In doing so, you are not canceling the "undo" record itself, but only the request to register it.