How do I capture the script editor output of a mel command, using in python?

We have some plugins that run as mel commands, and when things go wrong they spew useful info to the script editor, but return 0. How do I capture the script editor spew (not the return value) of a mel command, using python?

import pymel.core as pm
mel_cmd= 'print("hello world\\n");'
result=pm.mel.eval(mel_cmd) # no this is just the return value
print "the output of mel '{}' is: '{}'".format(mel_cmd, result)

#script editor output:
#hello world
#the output of mel 'print("hello world\n");' is: 'None'

so I came up with this clunky decorator function, but it feels…clunky.

def capture(function):
    """
    a decorator fn that captures output sent to the maya script editor
    :param function: a function whose script editor output we want to capture
    :return:
    """

    TEMP_FILE = os.getenv("TEMP") + ("/maya_script_editor_capture.txt")


    # empty the temp file so we only capture current command
    with open(TEMP_FILE, "w") as f:
        f.write("")

    #wrap the function that outputs to the script editor
    def captured_output(*args, **kw):
        pm.scriptEditorInfo(writeHistory=True, historyFilename=TEMP_FILE)
        result= function(*args, **kw)
        pm.scriptEditorInfo(writeHistory=False)
        with open(TEMP_FILE, "r") as f:
            history=f.read()
            return history
    return captured_output

@capture
def test():
    mel_cmd= 'print("hello world\\n");'
    pm.mel.eval(mel_cmd)

captured_result=test()
print("captured result: {}".format(captured_result))

Seems to give me what I want but I wonder if there’s some other way to redirect the script editor, so that I’m not writing and reading files just to get the text.

You might be able to use StringIO.StringIO() instance instead of the temp file.

1 Like

I’ll give StringIO a shot.

the plugin programmer explained that the plugin returns MStatus::kSuccess or MStatus::kFailure
but I can’t seem to capture a value from this using MEL (or python wrapped mel). the return values appear to be null /None

StringIO class instances can’t be passed as the historyFilename parameter.
odd that scriptEditorInfo() has no direct ‘give me your text’ function.

Ah, didn’t realize it took a file path and not a file object.
Yeah StringIO won’t work in that case.

Another option would be OpenMaya.MGlobal.executeCommand which lets you run a mel command and potentially capture the success/failure.
Pymel uses this under the hood to capture errors / convert to Exceptions.

What happens if you stick an imposter into sys.stdout in python… ? It will allow you to, for example, record the log there before passing it along. Does mel show up in that? never tried it , but perhaps someone has.

I was able to use MCommandMessage to capture a filepath from arnold. This doesn’t seem to work with the new api.

# capture stdout in a global var instead of writing to a file
# om2 doesn't work
global cmd_output
cmd_output = []

def _callback(nativeMsg, messageType, data):
    global cmd_output
    cmd_output.append(nativeMsg)

id = om.MCommandMessage.addCommandOutputCallback(_callback, None)

# fire the render
mc.arnoldRenderToTexture(
    folder=outputFolder, 
    shader=shader, 
    resolution=resolution, 
    aa_samples=samples, 
    filter='gaussian', 
    normal_offset=.1, 
    enable_aovs=False, 
    extend_edges=False,
    sequence= False
    )

# !! deregister the callback !!
# copy command output to string
om.MMessage.removeCallback(id)
# print cmd_output

# parse and check output for path
for line in cmd_output:
    path = line.split('[mtoa] Render to Texture : Rendered to ')[-1]

I happened across this problem again, cleaned up implementation a bit. Posting here in case anyone encounters.

import maya.api.OpenMaya as om
from contextlib import contextmanager

# Build a simple data class, initialize a simple list
class CustomData:
    def __init__(self):
        self.value = []

# Create a custom callback, this will be passed three arguments from MCommand message
def customCommandOutputCallback(message, filter, customData):
    if isinstance(customData, CustomData):
        customData.value.append(message)

# Setup context manager
@contextmanager
def capture_output():
    # setup the custom data
    data = CustomData()
    try:
        # >>>> IMPORTANT DO NOT PRINT<<<<
        # Any print statements in this block will create ain infinite loop and crash
        # you are trying to capture the the item you are printing...
        id = om.MCommandMessage.addCommandOutputCallback(customCommandOutputCallback, data)
        yield data
    finally:
        om.MCommandMessage.removeCallback(id)
        # print("Exiting the context manager")
        
    return data

# example
def get_queue(redo=False, strip=False):
    queue = []
    if redo:
         with capture_output() as output:
            mc.undoInfo(q=True, prq=True)
    else:
        with capture_output() as output:
            mc.undoInfo(q=True, pq=True)

    
    if output.value is not None:
        queue = output.value
        
        # optionally remove the queue id
        if strip:
            queue = [v.split(':')[-1].lstrip() for v in output.value]
    
    return queue

get_queue()
get_queue(redo=True)
2 Likes