Search code examples
python-2.7win32comcom+sas-jmpjsl

JMP 9 automation bug -- workarounds?


I'm using JMP 9.0.3 64-bit under Windows 7 and automating it from Python (EDIT: I've confirmed that the bug can equally be reproduced with VBScript automation, and still exists in JMP 11.0.0). My automation code is based on the JMP 9 Automation Guide. All the JMP9 PDFs seem now to have disappeared from the website.

This bug is becoming quite a show-stopper for me. I quite frequently need to manipulate tables in automation code and then exchange the table names with JSL code, and this bug makes it impossible to do so reliably. Has anyone else encountered it? Any known fixes or workarounds?

(I haven't seen many JMP/JSL questions on StackOverflow, but I'm posting here on the off-chance that there are some JMP-using lurkers. Originally posted on SAS's JMP forum: https://community.jmp.com/message/213132#213132)

The problem

the Document automation object has properties Name, FullName, and Path which are supposed to reflect the table name or file name of the associated JMP table. However, in many cases these properties turn out to be blank, despite the table having a non-blank name which can be accessed from JSL code, and despite the fact that the table automation object can in fact be retrieved using this name.

Demo code

Here's some Python code that demonstrates the bug. It creates a table using JSL, saves the name of this table, and looks up the table's automation object by name. It then checks whether table.Document.Name matches the known name of the table--which was just used to look it up!--and reports the cases where this doesn't hold. It does this 100 times and typically the name starts coming back blank after the first 2-4 iterations:

from win32com.client import gencache
mod = gencache.GetModuleForProgID("JMP.Application")
app = mod.Application()

okay_table = [None]*100

for ii in range(len(okay_table)):
    # Create a table in JMP
    app.RunCommand("show(%d); ::dt=New Table(); ::retval=dt<<Get Name()" % ii)

    # Retrieve the name of that just-created table from the JSL variable
    retval = app.GetJSLValue("retval")

    # Retrieve the automation object for that table, by name
    table = app.GetTableHandleFromName(retval)

    # Now, table.Document.Name **SHOULD** match retval, but
    # it may be blank due to the bug.

    if not any((table.Document.Name, table.Document.Path, table.Document.FullName)):
        print "table %d: got blank table.Document.Name=%s, Path=%s, FullName=%s" % (ii,
            table.Document.Name, table.Document.Path, table.Document.FullName)
        app.RunCommand("close(DataTable(::retval), nosave)")
        okay_table[ii]=False
    else:
        print "table %d: looks okay; Name=%s, FullName=%s, Path=%s" % (ii,
            table.Document.Name, table.Document.FullName, table.Document.Path)
        app.RunCommand('close(DataTable("%s"), nosave)' % table.Document.Name)
        okay_table[ii]=True

print "Number of bad tables: %d" % okay_table.count(False)

Typical output:

table 0: looks okay; Name=Untitled 304, FullName=Untitled 304.jmp, Path=Untitled 304.jmp
table 1: looks okay; Name=Untitled 305, FullName=Untitled 305.jmp, Path=Untitled 305.jmp
table 2: got blank table.Document.Name=, Path=, FullName=
table 3: got blank table.Document.Name=, Path=, FullName=
table 4: got blank table.Document.Name=, Path=, FullName=
...
table 98: got blank table.Document.Name=, Path=, FullName=
table 99: got blank table.Document.Name=, Path=, FullName=
Number of bad tables: 98

Solution

  • I've come up with a workaround, but it's an excruciatingly awkward one. In order to get the name of an automation table, I now do this:

    1. Get names of all JMP tables in a list by running JSL code.
    2. Use app.GetJSLValue to get this list into the automating application.
    3. Loop through the list of names one-by-one, looking up automation table objects by name using app.GetTableHandleFromName
    4. I then use an ugly kludge to compare the OLE object identity of each table to the OLE object identity of my target table. If they match, I return the name which I used to look it up.

    Code for my horrible ugly workaround:

    def GetTableName(app, table):
        if table.Document.Name:
            return table.Document.Name
        else:
            # Get names of all JMP tables
            app.RunCommand("""
                NamesDefaultToHere(1);
                ::_retval={};
                for(ii=1, ii<=NTable(), ii++,
                    InsertInto(::_retval, DataTable(ii)<<GetName())
                )""")
            tns = app.GetJSLValue("_retval")
    
            # See this thread for why == works here (http://thread.gmane.org/gmane.comp.python.windows/12977/focus=12978)
            for tn in tns:
                if table == self.app.GetTableHandleFromName(tn)
                    return tn
            else:
                raise Exception