Q

In previous sections we covered how to natively access Tcl, read/set variables, typecast variables, and making API calls. However, all of that was entirely based on “evaluating” Tcl code in Python. That is: evaluating each line of Tcl in its string format, and somehow typecasting the returning string back to Python objects.

There’s a certain degree of frustration with that. Yes, it is natively Tcl, yes it allows you to do anything and everything, but because the code to be evaluated is a string, when building the line, you must obey Python’s string formatting rules & use string substitution and such.

The Quartermaster

tcl module offers a magic quartermaster function that allows users to make Tcl calls as if they were Python objects, allowing object attribute chaining and support for mapping Python style arguments args``and``kwargs to Tcl’s positional and dashed argument styles.

../_images/q.jpg

A tribute to Desmond Llewelyn, Q from 1963 to 1999.

The magic Q function is built-in to each Interpreter object instance and can be accessed via either the q``or``Q attribute.

# Example
# -------
#
#   how to make fun of Q, 007 style.

from pyats import tcl

# create some procs/namespaces for use during this example
tcl.eval('''
    # for testing casting
    keylset klist a "some value"
    keylset klist b "more values"
    keylset klist c.d.e "nested keys"

    # for testing positional arguments
    proc testPositionalArgs {a b c} {
        return [list $a $b $c]
    }

    # for testing dashed arguments
    proc testDashedArgs {args} {
        return $args
    }

    # for testing mixed style arguments
    proc testMixedArgs {a b args} {
        return [list $a $b $args]
    }

    # for nesting namespaces
    namespace eval ::a {
        namespace eval b {
            proc c {args} {
                return $args
            }
        }
    }
''')

# what is this... Q?
tcl.q
# <pyats.tcl.Q magic, referring Tcl code>

# q is Q - the lowercase was only created to avoid pressing shift :)
assert tcl.q is tcl.Q
True

# setting and getting variables
tcl.q.set('myVar', 1)
ret = tcl.q.set('myVar')

# Q always performs a typecast (using Interpreter.cast_any) on the return
tcl.q.set('myVar')
1
tcl.q.set('klist')
KeyedList({'b': 'more values',
           'c': KeyedList({
                'd': KeyedList({
                    'e': 'nested keys'
                })
            }),
           'a': 'some value'})


# calling namespace APIS
# notice the chaining in arguments
result = tcl.q.a.b.c()

# calling with positional arguments
tcl.q.testPositionalArgs("pos 1", "pos 2", "pos 3")
# {pos 1} {pos 2} {pos 3}

# calling with kwargs: they get mapped to dashed args
tcl.q.testDashedArgs(arg_a = 'a string', arg_b = 1, arg_c = 1)
# -arg_a {a string} -arg_b 1 -arg_c 1

# calling with mixed arguments
tcl.q.testMixedargs('position 1',
                    'position 2',
                    arg_a = 'a string', arg_b = 1, arg_c = 1)
# {position 1} {position 2} {-arg_b 1 -arg_c true -arg_a {a string}}

Regulations

The Q mechanism works by converting Python calls into specific Tcl syntax. The following rules are followed:

  • Q calls are relative to the current Tcl namespace (usually global ::).

    # refer to Tcl's set command (built-in)
    tcl.q.set
    
  • absolute namespaces in Tcl are represented as Q attribute chains.

    # refers to Tcl ::a::b::c procedure
    tcl.q.a.b.c
    
  • call procedure as if you are making a Python call using ().

    # making a call
    tcl.info('patchlevel')
    
  • Python *args arguments are converted into Tcl positional arguments, respecting the original argument position.

    # notice that set takes two arguments: name and value
    tcl.q.set('varName', 'varValue')
    
  • Python **kwargs keyword arguments are converted into Tcl dashed arguments. Order is not preserved, as dashed arguments are not order-sensitive.

    # representation -a 1 -b 2 dashed arguments
    tcl.q.testProc(a = 1, b = 2)
    
  • When *args``and``**kwargs are used together, positional arguments comes first, keywords/dashed arguments comes last.

  • All arguments are converted from Python objects to Tcl string forms using tclstr casting API. See data casting section for details.

Auto-Cast

By default, all Q function calls are always casted into Python objects using Interpreter.cast_any functionality. This allows maximum python-to-python look & feels. This functionality can be turned off using cast_ = False argument.

# Example
# -------
#   Q casting on/off

from pyats import tcl.

tcl.eval('set myVar 1')

# by default, Q casting is always on:
type(tcl.q.set('myVar'))
# <type 'int'>

# turn it off using cast_ argument
type(tcl.q.set('myVar', cast_ = False))
# <type 'str'>

Note

technically, cast_ uses off a “potential” argument that can be sent to the actual function called by Tcl. Thus, a trailing _ is added in order to minimize the chance of such collisions. If you do actually find a case where a Tcl API has an argument named cast_, please let us know.

Limitations

There are still some fundamental differences between Tcl and Python syntax, leading to corner conditions that cannot be handled even by the all-mighty Q.

  • Python identifiers are limited to A-Z,``a-z``,``0-9``and``_``. Thus, any procedure/namespace names with characters outside of this allowed set cannot be called with Q function.

  • Any Tcl procedure/namespaces with names the same as Python reserved keywords, statements and operators cannot be called. Eg: pass,``return``,``def``.

# Example
# -------
#
#   some example that cannot be called with Q

from pyats import tcl

tcl.eval('''
    proc procedureWith:Colon {} {}

    proc procedureWith-Dash {} {}

    namespace eval namespaceWith/\Slash {} {}

    # the null procedure
    proc {} {} {}
''')

# try to call them
tcl.q.procedureWith:Colon()
#   File "<stdin>", line 1
#     tcl.q.procedureWith:Colon()
#                        ^
# SyntaxError: invalid syntax

tcl.q.procedureWith:Colon
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# NameError: name 'Dash' is not defined

tcl.namespaceWith/\Slash
#   File "<stdin>", line 1
#     tcl.namespaceWith/\Slash
#                            ^
# SyntaxError: unexpected character after line continuation character

# and I honestly don't know how to call the null function in Python
# ...

The only workaround for the above limitations is to continue using the basic Interpreter.eval() method, and evaluate them as strings.

# continuing from the above example ...

tcl.eval('procedureWith:Colon')

tcl.eval('procedureWith-Dash')

tcl.eval('namespaceWith/\Slash')

tcl.eval('{}')

Hint

even though Tcl allows for special characters in procedure/namespace names, it is still considered extremely bad practice. Don’t do it.

Examples

Here’s some practical day-to-day usage examples using Q magic.

# Example
# -------
#
#   some actual examples of how to abuse Q
#   (don't forget to return his gadgets in a broken state)

import os
from pyats import tcl

# source some files
tcl.q.source(os.path.join('path','to','lib.tcl'))

# delete an array element
tcl.q.unset('myArray(index)')

# list appends
tcl.q.lappend('myList', 'value 1', 'value 2')

# keyed list operations
tcl.q.keylset('myKlist', 'key_a', 'value', 'key.subkey', 'value')

# load up some packages
tcl.q.package('require', 'Expect')