Nemo
Nemo

Reputation: 23

Parsing associate array returned from TCL in Python

I am having one requirement for calling TCL scripts from Python. My requirement is to call a TCL proc from python. The TCL proc returns the result as an array. I need to parse the returned array in Python to form a dictionary. Below is a working code. Is there any more efficient way to do the same?

Python Code

import tkinter
import shlex

r=tkinter.Tk()
r.tk.eval('source proc.tcl')
arr = []
arr = r.tk.eval('processarray arr')
print(arr)
j=0
for i in arr.split(' '):
    print(i)
d = {}
ssplit = arr.split()
print(len(ssplit))
print(len(ssplit)/2)
j=int(len(ssplit)/2)
k=0
for i in range (j):
    key = ssplit[k]
    d[key] = ssplit[k+1]
    k += 2
print(d)

TCL Code

proc processarray { $arr } {
    array set arr " arrlist 25645"
    set arr(0) "11"
    set arr(1) "10"
    set arr(2) "20"
    set arr(3) "30"
    set arr(4) "40"
    return [array get arr]
}

Expected Result

{'4': '40', '0': '11', '1': '10', 'arrlist': '25645', '2': '20', '3': '30'}

Upvotes: 0

Views: 805

Answers (3)

Donal Fellows
Donal Fellows

Reputation: 137717

You can simplify things for yourself a lot by using call instead of eval.

import tkinter

r = tkinter.Tk()
# Putting the Tcl code I've used inline
r.tk.eval("""
    proc processarray { $arr } {
        array set arr " arrlist 25645"
        set arr(0) "11"
        set arr(1) "10"
        set arr(2) "20"
        set arr(3) "30"
        set arr(4) "40"
        # CRITICAL! Must return a list, not a dict
        return [list {*}[array get arr]]
    }
""")
r.tk.wantobjects(True)  # This is the default setting...
arr = r.tk.call('processarray', 'arr')
# This is a bit of magic to turn a pair-list into a dict, based on
# https://stackoverflow.com/questions/47202751/splitting-python-string-into-list-of-pairs
c = {k: v for k, v in zip(arr[:-1:2], arr[1::2])}
print(c)

The critical things here are that:

  1. we use call (which is a better option for calling single commands and does argument boundary preservation, etc.),
  2. we've got wantobjects(True) (actually on by default), and
  3. we get the Tcl side to force the result to be a list ([list {*}…]) and we post-process that list on the Python side to get a dictionary (this is because Tkinter doesn't do dict mapping for you for some reason; that's a Tkinter bug/missing feature).

When I run this, I get this output:

{4: '40', 0: '11', 1: '10', 'arrlist': '25645', 2: '20', 3: '30'}

That's as expected; Tcl's arrays have never been order-preserving and are documented to not be so. (I've no idea why Tkinter's converted some keys to ints.)

Upvotes: 0

Shawn
Shawn

Reputation: 52529

Python's Tkinter package isn't documented very well, so I had to dig into the source for this...

There is a _splitdict() function that will, given a string with an even number of tcl words like that returned by Tcl's array get, return it as a python dictionary. I don't really know Python, but I bet the underscore and clunky interface means it's internal and you're not supposed to use it. But there doesn't seem to be a public alternative:

>>> import tkinter
>>> tcl = tkinter.Tcl()
>>> tkinter._splitdict(tcl, 'foo 1 bar 2 baz {3 4}')
{'bar': '2', 'foo': '1', 'baz': '3 4'}

Alternatively, you can probably just write your function that does what it does internally. The important bit is the tk.splitlist('some string'), which breaks its argument up into words according to tcl rules - needed to properly handle spaces or anything like that in the individual words of the list that array get returns (See above for an example).

A (Untested), trimmed down version. Original can be found in the Python source

def splitdict(v):
    """Return a properly formatted dict built from Tcl list pairs.
    Tcl list is expected to contain an even number of elements.
    """
    t = r.tk.splitlist(v)
    if len(t) % 2:
        raise RuntimeError('Tcl list representing a dict is expected '
                           'to contain an even number of elements')
    it = iter(t)
    dict = {}
    for key, value in zip(it, it):
        key = str(key)
        dict[key] = value
    return dict

Upvotes: 3

furas
furas

Reputation: 142814

I never used TCL but I run this code and get from TCL

4 40 0 11 1 10 arrlist 25645 2 20 3 30

I can convert it to dictionary using range(len()) and [i:i+2].
Maybe it is not popular to use range(len()) but in this situation it will be very useful.

import tkinter

r = tkinter.Tk()
r.tk.eval('source proc.tcl')

arr = []
arr = r.tk.eval('processarray arr')
#print(arr)

items = arr.split()

data = {}
for i in range(0, len(items), 2):
    key, val = items[i:i+2]
    data[key] = val
    
print(data)

Result

{'4': '40', '0': '11', '1': '10', 'arrlist': '25645', '2': '20', '3': '30'}

EDIT: I can do this also with iter() and zip()

import tkinter

r = tkinter.Tk()
r.tk.eval('source proc.tcl')

arr = []
arr = r.tk.eval('processarray arr')
#print(arr)

items = arr.split()

data = {}
it = iter(items) # create iterator 

for key, val in zip(it, it):  # use `zip()` to get two elements from list
    data[key] = val
    
print(data)

Upvotes: 0

Related Questions