[Python] Looking for some advice with a pattern. "Conditional" Context Manager

I’d trying to come up with something like a context manager that can branch based on input.

For example, say I am in Maya and I would like to early out if the selection is None. Normally I would do something like:


sel = cmds.ls(sl=True)
if not sel:
    cmds.warning('Nothing Selected!')
else:
    myFunc(sel)

If I wanted to personalize the message I would do something like this:


def myFunc(sel):
    if not sel:
        cmds.warning('myFunc requires at least 1 object selected.')
        return
    # stuff and things.

I’d like some kind of wrapper that could execute a block of code only if the input passes a test.

Maybe something like:


with somethingSelected(cmds.ls(sl=True)) as sel:
    myFunc(sel)

where myFunc would not be executed if the current selection is None(it would also handle printing the warning).
I tried this with a context manager, but they always run the code they wrap. After reading up on context managers, they aren’t designed to handle conditions.

I came across the conditional package, which seems like it is designed specifically to address the same problem, but I’d like to use a home-grown solution.

Any ideas? Maybe a decorator with it’s own arguments would be a better choice?

A decorator is the more natural framework here, since the ‘context’ functionality isn’t really what you’re asking for. You could easily do a decorator that enforces some kinds of inputs and raises or aborts if it does not get what it wants – that’s a nice way of centralizing behavior. Here’s one I use that converts pynodes to strings for use with functions that don’t want pymel:


def accept_pynode(func):
    '''
    Decorator converts input arguments from pm to cmds if needed
    '''
    @wraps(func)
    def py_wrapped(*args, **kwargs):
        args = (i.fullPath() if hasattr(i, 'fullPath') else str(i) for i in args)

        return func(*args, **kwargs)
    return py_wrapped

It’s a lot easier than doing the conversion inside a zillion different functions. You could just add a line after the ‘args’ line to raise or print and bail (raising is better, IMHO, but it’s a style thing):


def accept_pynode(func):
    '''
    Decorator converts input arguments from pm to cmds if needed
    '''
    @wraps(func)
    def py_wrapped(*args, **kwargs):
        args = (i.fullPath() if hasattr(i, 'fullPath') else str(i) for i in args)
        if len(args) < 1:
           raise "no valid objects"
        return func(*args, **kwargs)
    return py_wrapped

If you want custom messages, you can grab the doc string of the wrapped function and scrape the message from there, or use a decorator class which takes the message as an argument

thanks for the tips! Quick question - what is this line doing?


@wraps(func)

edit: nevermind, found this very informative answer on SO.

One more question, let’s say I am not going to go the route of raising an exception is the selection is None, or empty. I Just want to print a warning and exit silently.

what would I set the return Value to in the decorator? A function that does nothing? None type?

In that case, you would have to write that inner function to early exit with the warning. In my example:


def accept_pynode(func):
    '''
    Decorator converts input arguments from pm to cmds if needed
    '''
    @wraps(func)
    def py_wrapped(*args, **kwargs):
        args = (i.fullPath() if hasattr(i, 'fullPath') else str(i) for i in args)
        if len(args) < 1:
           cmds.warning ("no valid objects")
           return []
        return func(*args, **kwargs)
    return py_wrapped

the ‘right’ return is a matter of taste. I usually use none when the function returns a single value, but empty list when the function returns a list. That way callers can loop without special casing.