# Project RoboNest - Nestable/Breakable For Loops + Basic While Loops for Robot Framework

I'm implementing things in Robot Framework that it does not support by default, including nestable For Loops and, now, While Loops. I'm looking for readability, algorithm, methodology, and/or performance suggestions.

The While Loop is not as advanced as the For loop is yet. I'm eventually looking to implement nesting the While Loop and breaking it on a command, but at present it will take any Robot Framework variable (assuming it's an integer) and loop until it reaches the specified breaking point. The custom keyword Increment plays the part of a line of internal code incrementing the value by a specified number. At present, Increment only supports + and - with any integer, but I can easily make it support * and / for more advanced increments.

The following code works, but as (still) a relative novice in Python I don't know all the tricks that could be used to optimize this loop structure and logic. The general idea, for those who have not read my other two RobotNest posts, is to allow the user to write nestable For and While loops in Robot Framework.

My basic methodology is as follows:

• The user writes the Loop as a single keyword with a short list of required arguments followed by whatever keywords they want, using ... at the beginning of the line followed by \\, once for every inner loop.
• The program loops through the list of keywords and re-formats the Loop into a format usable by Robot Framework's run_keywords method located under the BuiltIn library, swapping \\ instances for AND.
• The program then runs the keywords through a custom run_keywords method (mirrors BuiltIn().run_keywords) so that I can control what keywords are executed when and break the loop if I need to. This method loops through the list of keywords and runs them based on their arguments individually.
• Within the main loop, the program dynamically writes the index variable (differs in execution between For and While variants) to all keyword arguments that call it.

This methodology seems very inefficient to me. By the end of the program, I've looped through my list of keywords once statically and around 4 or 5 for every iteration of the loop itself. This is why I'm looking for algorithm suggestions.

In addition, I'd like to get away from using \\ to delineate lines, but unless I figure out how the compiler takes Robot Framework's code and says "this is what it equates to in Python", that probably won't be happening.

Here's my code as it currently stands:

from robot.libraries.BuiltIn import BuiltIn

# TODO: Create new types of For Loops
# TODO: Expand While Loop
# TODO: Create Do-While Loops

class Loops(object):

def __init__(self):
self.selenium_lib = BuiltIn().get_library_instance('ExtendedSelenium2Library')
self.internal_variables = {}

def for_loop(self, loop_type, start, end, index_var, *keywords):
#   Format the keywords
keywords = self._format_loop(*keywords)
#   Clean out the internal variables from previous iterations
self.internal_variables = {}
#   If I'm given a range of numbers...
if loop_type == 'IN RANGE':
#   This is the actual looping part
for loop_iteration in range(int(start), int(end)):
keyword_set = self._index_var_swap(loop_iteration, index_var, *keywords)
#   If it's a one-keyword list with no arguments, then I can use the fastest possible keyword to run it
if len(keyword_set) == 1:
BuiltIn().run_keyword(keyword_set)
#   If it's a one-keyword list with arguments, then I can use a faster keyword to run it
elif 'AND' not in keyword_set:
#   If the keyword isn't Exit If, then I can just run it normally.
if keyword_set[0].lower() != 'exit if':
BuiltIn().run_keyword(*keyword_set)
#   If the keyword is Exit If, then I need to evaluate the keyword differently.
elif BuiltIn().run_keyword(*keyword_set):
break
#   If it's a multiple-keyword list, then I have to use Run Keywords
else:
temp = self._run_keywords(*keyword_set)
if not temp:
break

def while_loop(self, comparison, *keywords):
#   Format the input
comparison = str(comparison).replace(" ", "")
#   Clean out the internal variables from previous iterations
self.internal_variables = {}
#   Create an internal variable to deal with a changing index
self._assign_internal_variable("while_loop_var", comparison[:Loops()._find_bool(comparison)])
comp = comparison
while Loops()._evaluate_boolean_string(comp):
#   Format the keywords
k_words = self._format_loop(*keywords)
self._while_var_swap(*k_words)
self._run_keywords(*k_words)
comp = str(self.internal_variables["while_loop_var"]) + str(comparison[Loops()._find_bool(comparison):])

def exit_if(self, condition):
return self._evaluate_boolean_string(condition)

def increment(self, inc):
operator = inc[0]
if self._is_number(inc[1:]):
if operator == '+':
self.internal_variables["while_loop_var"] =\
int(self.internal_variables["while_loop_var"]) + int(inc[1:])
elif operator == '-':
self.internal_variables["while_loop_var"] =\
int(self.internal_variables["while_loop_var"]) - int(inc[1:])
return False

def _format_loop(self, *keywords):
keywords = list(keywords)   # I need to format the keywords as a list.
changed = False             # Whether or not I changed anything in the previous iteration.
index = 0                   # The item index I'm at in the list of keywords
del_list = []               # The list of items I need to delete
swap_list = []              # The list of items i need to swap to AND for the use of Run Keywords

def _new_variable():
#   Default to a variable declaration of 'name='
t = 1
#   If my variable declaration is 'name ='
if x[-2:] == ' =':
#   Reflect that in the value of t
t = 2

#   Count the number of cells until the end of the line
length = self._deliminator_search(index, x, *keywords)

if length == 3 and not BuiltIn().run_keyword_and_return_status("Keyword Should Exist", keywords[index + 1]):
#   If I'm assigning a value to my variable
self._assign_internal_variable(x[:-t], str(keywords[index + 1]))

elif length == 3:
#   If I'm assigning the result of a keyword without any arguments
self._assign_internal_variable_to_keyword(keywords[index][:-t], str(keywords[index + 1]))
else:
#   If I'm assigning the result of a keyword with arguments
self._assign_internal_variable_to_keyword(keywords[index][:-t], keywords[index + 1],
keywords[index + 2:index + (length - 1)])

#   Add the variable declaration code to the delete list.
del_list.extend(range(index - 1, index + length))

#   For each argument
for x in keywords:
#   Format it to a string
x = str(x)
#   Assign new variables
if x[-1:] == '=':
_new_variable()

#   If the previous element was not changed...
if not changed:
#   If the current item is not the last one on the list...
if x != len(keywords) - 1:
#   If the current item is a deliminator...
if x == '\\':
#   If the next item is a deliminator, delete this item and set changed to True
if keywords[int(index) + 1] == '\\':
del_list.append(index)
changed = True
#   If the next item is not a deliminator...
else:
#   If this isn't the first deliminator on the list, swap it to an 'AND'
if index != 0:
swap_list.append(index)
changed = True
#   If this deliminator is in position index=0, just delete it
else:
del_list.append(index)
changed = True
#   If the current element is not a deliminator, then I don't need to touch anything.
#   If the current element is the last one, then I don't need to touch anything
#   If the previous element was changed, then I don't need to "change" this one...
elif changed:
changed = False
#   ...but if it's a deliminator then I do need to set it up for the inner for loop it means.
if keywords[index] == '\\':
keywords[index] = keywords[index]*2
index = index + 1   # Advance the index

# These actually do the swapping and deleting
for thing in swap_list:
keywords[thing] = 'AND'
del_list.reverse()
for item in del_list:
del keywords[item]

# I also need to activate my variables for this set of keywords to run.
keywords = self._activate_variables(*keywords)

return keywords

def _assign_internal_variable(self, variable_name, assignment):
# This keyword works like any other keyword so that it can be activated by BuiltIn.run_keywords
self.internal_variables[variable_name] = assignment

def _assign_internal_variable_to_keyword(self, variable_name, keyword, *arguments):
# Uses assign_internal_variable to simplify code.
self._assign_internal_variable(variable_name, BuiltIn().run_keyword(keyword, *arguments))

def _activate_variables(self, *keywords):
#   Initialize variables
keywords = list(keywords)   # Cast keywords as a List
index = 0                   # The index of the keyword I'm looking at

#   For each keyword
for keyword in keywords:
keyword = str(keyword)  # Cast keyword as a String
assignment = False      # Whether or not the found variable name is in a variable assignment
for key in self.internal_variables.keys():
key = str(key)      # Cast key as a String
#   If I can find the key in the keyword and it's not an assignment...
if keyword.find(key) > -1 and not assignment:
#   ...replace the text of the key in the keyword.
keywords[index] = keyword.replace(str(key), str(self.internal_variables[key]))
#   If the keyword I'm looking at is an assignment...
#   ...then my next keyword is going to definitely be a known variable, so I don't want to touch it.
assignment = True
#   If the keyword I'm looking at is not an assignment...
else:
#   ...set assignment to False just in case the previous one happened to be an assignment.
assignment = False
index = index + 1   # Advance the index
#   NOTE: Replaces the EXACT text, even if it's in another keyword or variable, so be very careful
return keywords     # Return the list of keywords to be used in the format loop

def _while_var_swap(self, *keywords):
#   Format the keywords as a list for iteration
keywords = list(keywords)
index = 0
#   For every line in keywords
for line in keywords:
#   Replace all instances of the index_var in the string with the loop iteration as a string
keywords[index] = str(line).replace('while_loop_var', str(self.internal_variables["while_loop_var"]))
index = index + 1
return keywords

@staticmethod
def _is_boolean_string(string_in):
# For all of the possible Boolean parameters...
for param in ['!', '<', '>', '=', '==']:
# Return whether or not the parameter is in the string.
if str(param) in str(string_in):
return True
return False

@staticmethod
def _evaluate_boolean_string(condition):
def _eval(arg_1, arg_2):
if (t[2][1] or t[2][2]) and not (t[2][1] and t[2][2]):  # If it has either > or < in it, but not both
if t[2][3]:  # If it has = in it
if t[2][1]:  # If it's >=
return arg_1 >= arg_2
else:  # If it's <=
return arg_1 <= arg_2
else:
if t[2][1]:  # If it's <
return arg_1 < arg_2
else:  # If it's >
return arg_1 > arg_2
elif t[2][4]:  # If it's ==
return arg_1 == arg_2
elif t[2][0] and t[2][3]:  # If it's !=
return arg_1 != arg_2
else:  # In case of Tester
return False

# Cast the condition as a string with no whitespaces
condition = str(condition).replace(" ", "")
# Initialize the t-table with default values
t = [[0, 1, 2, 3, 4],
['!', '<', '>', '=', '=='],
[False, False, False, False, False]]
# Find the start of the Boolean expression
start, t = Loops()._find_bool(condition, t)
# Evaluate the expression
return _eval(condition[:start], condition[start + sum(t[2]):])

@staticmethod
def _find_bool(comparison, tin=None):
if tin is None:
t = [[0, 1, 2, 3, 4],
['!', '<', '>', '=', '=='],
[False, False, False, False, False]]
else:
t = tin
#   Initialize the starting index as the last index in condition
begin = len(comparison)
#   For all of the parameters...
for index in t[0]:
#   Find the location of the start of the boolean parameters
temp = comparison.find(str(t[1][int(index)]))
#   If the location exists and is less than start...
if temp <= begin and temp != -1:
#   Set start to the location
begin = temp
t[2][int(index)] = True
#   If the input was bad, return -1 and don't change the t table
if sum(t[2]) > 2 or sum(t[2]) == 0 or begin == len(comparison):
if tin is None:
return -1
else:
return -1, tin
else:
if tin is None:
return begin
else:
return begin, t

@staticmethod
def _run_keywords(*keys):
#   Find the end of the current keyword
def _and_search(start, key='no key yet'):
# I'm starting with a false key that is never 'AND'
and_index = 1       # I never want to start on an 'AND'
#   While the current key isn't 'AND' and I'm not at the end of the list of keywords...
while key != 'AND' and and_index + start != len(key_list):
#   Set the current key equal to the next key in the list.
key = key_list[int(start) + and_index]
and_index = and_index + 1   # Advance the index

#   If the final key is an 'AND'...
if key == 'AND':
return and_index - 1    # Return the keyword length minus the 'AND'
#   Otherwise...
else:
return and_index    # Return the keyword length

def _split_keyword_list():
first = True    # We always start at the first cell of a keyword/argument set.
index = 0       # The item index I'm at in the list of keywords
#   For each word in the list of keywords/arguments...
for word in key_list:
#   If it's the first word...
if first:
#   Append the keyword
keywords.append(key_list[int(index):(index + _and_search(index))])
first = False   # Set first to False
#   If it's any other word I don't need to append it, but...
else:
#   If it's the last word in the keyword...
if _and_search(index, word) == 0:
#   The next keyword must be the first.
first = True
index = index + 1  # Advance the index

# I need to format the keywords as a list and instantiate the array of keywords as an empty array.
key_list, keywords = list(keys), []

#   Split list into keyword/arg sets
_split_keyword_list()

#   For each key/arg item in the list...
for keyword in keywords:
#   Run the keyword with its arguments
if keyword[0].lower() != 'exit if':
BuiltIn().run_keyword(keyword[0], *keyword[1:])
elif Loops()._is_boolean_string(keyword[1]) and BuiltIn().run_keyword(keyword[0], *keyword[1:]):
return False
return True

@staticmethod
def _index_var_swap(loop_iteration, index_var, *keywords):
#   Format the keywords as a list for iteration
keywords = list(keywords)
index = 0
#   For every line in keywords
for line in keywords:
#   Replace all instances of the index_var in the string with the loop iteration as a string
keywords[index] = str(line).replace(str(index_var), str(loop_iteration))
index = index + 1
return keywords

@staticmethod
def _var_swap(old_var, new_var, keywords):
index = 0
#   For every line in keywords
for line in keywords:
#   Replace the all instance of the oldVar in the string with newVar as a string
keywords[index] = str(line).replace(str(old_var), str(new_var))
index = index + 1
return keywords

@staticmethod
def _deliminator_search(start, keyword, *keywords):
index = 0
while keyword != '\\' and keyword != '\\\\':
keyword = keywords[int(start) + index]
index = index + 1
return index

@staticmethod
def _is_number(s):
try:
float(s)
return True
except ValueError:
return False


And here's the working Robot Framework code that I use to test it:

*** Settings ***
Library     ExtendedSelenium2Library
Library     Loops.py

*** Variables ***
${blue_squadron} = Blue${gold_squadron} =      Gold
${green_squadron} = Green${red_squadron} =       Red
${test_index_1} = 0${test_index_2} =       3

*** Test Cases ***
Test For Loop IN RANGE
For Loop    IN RANGE    0   1   INDEX0
...     \\  For Loop    IN RANGE    1   6   INDEX1
...     \\  \\  {standing_by}=      standing by
...     \\  \\  Run Keyword If      INDEX1 == 1     Log to Console  This is ${red_squadron} Leader standing by ... \\ \\ Run Keyword Unless INDEX1 == 1 Log to Console${red_squadron} INDEX1 {standing_by}
...     \\  For Loop    IN RANGE    1   6   INDEX2
...     \\  \\  standing_by_2 =     standing by
...     \\  \\  Run Keyword If      INDEX2 == 1     Log to Console  This is ${gold_squadron} Leader standing by ... \\ \\ Run Keyword Unless INDEX2 == 1 Log to Console${gold_squadron} INDEX2 standing_by_2
...     \\  For Loop    IN RANGE    1   6   INDEX3
...     \\  \\  standing_by_3=      Get Blue Squadron
...     \\  \\  Run Keyword If      INDEX3 == 1     Log to Console  This is ${blue_squadron} Leader standing by ... \\ \\ Run Keyword Unless INDEX3 == 1 Log to Console${blue_squadron} INDEX3 standing_by_3
...     \\  For Loop    IN RANGE    1   6   INDEX4
...     \\  \\  standing_by_4 =     Get Green Squadron   null input
...     \\  \\  Run Keyword If      INDEX4 == 1     Log to Console  This is ${green_squadron} Leader standing by ... \\ \\ Run Keyword Unless INDEX4 == 1 Log to Console${green_squadron} INDEX4 standing_by_4

Test IN RANGE Edge Case 1 - Single Keyword with Single Argument
For Loop    IN RANGE    0   1   INDEX0
...     \\  Log to Console  testlog

Test For Loop Exit
For Loop    IN RANGE    0   3   INDEX0
...     \\  Log to Console  INDEX0
...     \\  Exit If     INDEX0 == 1

Test While Loop
While Loop  ${test_index_1} < 3 ... \\ Log to Console Index: while_loop_var ... \\ Increment +1 While Loop${test_index_2} > 0
...     \\  Log to Console  Index: while_loop_var
...     \\  Increment   -1

*** Keywords ***
[Return]    standing by

[Arguments]     ${text} [Return] standing by  Language: Python 2.7.13, Robot Framework 3.0.2 (Python 2.7.13 on win32) Dependencies: BuiltIn Library, ExtendedSelenium2Library • The two backslashes seems rather cumbersome. Have you considered some other character or characters? For example, : or :: in a cell by itself? From a human factors perspective, those are easier to type and read. Also, why are you using expressions like INDEX == 1 rather than ${INDEX} == 1? – Bryan Oakley Nov 10 '17 at 21:40
• Originally, I was trying to stay as close to the original syntax of a Robot Framework For Loop as possible. Since the original uses \ and I was reading that as a cell into a language that interprets it as an escape character, I had to use two. Recently I've started looking into using the BuiltIn().keyword_should_exist method to find lines, but I'm running into issues with that so far. I'm using the Python-like expressions because Robot Framework-style variables are compiled to their numbers as they're being sent to Python to be run, while non-Robot Framework syntax can work in the loop. – Brandon Olson Nov 10 '17 at 21:53
• @BryanOakley I attempted to use :: in a cell by itself, but for some reason Robot Framework or Python converts :: :: to ::::, which causes a lot more problems. – Brandon Olson Nov 14 '17 at 22:42
• Neither python nor robot will just automatically convert one string to another unless you did something wrong. Did you have two or more spaces after :: before the next cell? – Bryan Oakley Nov 14 '17 at 22:45
• Yes. The same thing happens with | and _. – Brandon Olson Nov 14 '17 at 22:48

After a significant amount of optimization, I got it... mostly. The main looping variable for the While Loop is still clunky (still working through BuiltIn's method of accessing Robot Framework-style variables), but it's about 1/6 as long as the original with the same functionality, and runs significantly faster with less overhead. I'm now working on nesting the While loops.

from robot.libraries.BuiltIn import BuiltIn

class Loops(object):
def __init__(self):
self.iv = {}

def loop(self, loop_type, a, b, c, *keywords):
def _find_bool(j):  # Used to find the next boolean operator
return min([j.find(i) for i in ['!', '<', '>', '=', '=='] if (-1 < j.find(i) < len(j))])
self.iv = {}  # Clean out internal variables
if loop_type.upper() == 'FOR RANGE':    # A For loop over a range of numbers.
keywords = self._format(*keywords)     # Format the keywords
for loop_iteration in range(int(a), int(b)):  # The actual looping part
proceed = self._run_keywords(*[k.replace(c, str(loop_iteration)) for k in keywords])
if not proceed:     # If the loop decides to break,
break           # break the loop.
elif loop_type.upper() == 'WHILE':      # A While loop with a condition.
a = str(a).replace(" ", "")         # Format the condition with no spaces.
self.iv["{{wlv}}"] = a[:_find_bool(a)]   # Create the wlv as an internal variable.
keywords = self._format(*([b, c] + list(keywords)))     # B and C inputs are actually part of the keywords.
while BuiltIn().evaluate(a):        # The actual looping part.
self._run_keywords(*keywords)   # Run the keywords with Loops's Run Keywords to control it better.
a = str(self.iv["{{wlv}}"]) + str(a[_find_bool(a):])     # Update the wlv

def increment(self, inc):
if inc[0] == '+':       # If I'm incrementing up, add one to the while loop var
self.iv["{{wlv}}"] = str(int(self.iv["{{wlv}}"]) + int(inc[1:]))
elif inc[0] == '-':     # If I'm incrementing down, subtract one from the while loop var
self.iv["{{wlv}}"] = str(int(self.iv["{{wlv}}"]) - int(inc[1:]))

@staticmethod
def _format(*k):
kl = list(k)                                                # Format keywords as list
swap = [i for i, x, in enumerate(kl) if x == '\\' and (kl[i - 1] != '\\' and kl[i + 1] == '\\')]
delete = [i for i, x, in enumerate(kl) if x == '\\' and kl[i - 1] == '\\' or i == 0]
kl = ['\\\\' if i in swap else x for i, x in enumerate(kl)]    # Swap '\\' for '\\\\'
return [k for i, k, in enumerate(kl) if i not in delete]    # Delete

def _run_keywords(self, *kl):
ks = [-1] + [i for i in range(0, len(kl) - 1) if kl[i] == '\\'] + [len(kl)]   # Find the deliminators
for i in range(0, len(ks) - 1):             # For each deliminator, get the name and arguments of the keyword
name, args = kl[ks[i] + 1], kl[ks[i] + 2:ks[i + 1]]
if str(name)[-1:] == '=':               # If it's an assignment, assign it.
self._assign_internal_variable(name.rstrip(' ='), *args)
elif str(name).lower() == 'exit if':    # If it's an exit if statement, break the For Loop.
if BuiltIn().evaluate(args[0], modules='os,sys'):
return False
else:                                   # Otherwise, run the keyword
for k in self.iv.keys():    # Activate internal variables
args = [x.replace(k, self.iv[k]) for x in args]
BuiltIn().run_keyword(name, *args)          # Run the keyword
return True     # Required for "exit if" keyword when loop breaking is installed

def _assign_internal_variable(self, variable_name, assignment, *arguments):
if BuiltIn().run_keyword_and_return_status("Keyword Should Exist", assignment):     # If it's a keyword
self.iv[variable_name] = BuiltIn().run_keyword(assignment, *arguments)  # Assign the result of the keyword
else:   # If it's a simple value
self.iv[variable_name] = assignment     # Save it to the variable name


And the test Robot Framework code:

*** Settings ***
Library     Loops.py

*** Variables ***
${blue_squadron} = Blue${gold_squadron} =      Gold
${green_squadron} = Green${red_squadron} =       Red
${standing_by} = standing by${test_index} =         0
${test_index_0} = 0${test_index_1} =       100
${test_index_2} = 3 *** Test Cases *** Test For Loop IN RANGE Loop FOR RANGE 0 1 INDEX0 ... \\ Log to Console Start ... \\ Loop FOR RANGE 1 6 INDEX1 ... \\ \\ {standing_by}= standing by ... \\ \\ Run Keyword If INDEX1 == 1 Log to Console This is${red_squadron} Leader standing by
...     \\  \\  Run Keyword Unless  INDEX1 == 1     Log to Console  ${red_squadron} INDEX1 {standing_by} ... \\ Loop FOR RANGE 1 6 INDEX2 ... \\ \\ standing_by_2 = standing by ... \\ \\ Run Keyword If INDEX2 == 1 Log to Console This is${gold_squadron} Leader standing by
...     \\  \\  Run Keyword Unless  INDEX2 == 1     Log to Console  ${gold_squadron} INDEX2 standing_by_2 ... \\ Loop FOR RANGE 1 6 INDEX3 ... \\ \\ standing_by_3= Get Blue Squadron ... \\ \\ Run Keyword If INDEX3 == 1 Log to Console This is${blue_squadron} Leader standing by
...     \\  \\  Run Keyword Unless  INDEX3 == 1     Log to Console  ${blue_squadron} INDEX3 standing_by_3 ... \\ Loop FOR RANGE 1 6 INDEX4 ... \\ \\ standing_by_4 = Get Green Squadron${standing_by}
...     \\  \\  Run Keyword If      INDEX4 == 1     Log to Console  This is ${green_squadron} Leader standing by ... \\ \\ Run Keyword Unless INDEX4 == 1 Log to Console${green_squadron} INDEX4 standing_by_4

Test IN RANGE Edge Case 1 - Single Keyword with Single Argument
Loop    FOR RANGE   0   1   INDEX0
...     \\  Log to Console  testlog

Test For Loop Exit
Log to Console  Start
Loop    FOR RANGE   0   3   INDEX0
...     \\  Log to Console  INDEX0
...     \\  Exit If     INDEX0 == 1

Test While Loops
Log to Console  Start
Log to Console  Testing 0 < 3
Loop    WHILE   ${test_index_0} < 3 ... \\ Log to Console Index: {{wlv}} ... \\ Increment +1 Log to Console Testing 3 > 0 Loop WHILE${test_index_2} > 0
...     \\  Log to Console  Index: {{wlv}}
...     \\  Increment   -1
Log to Console  Testing 0 <= 3
Loop    WHILE   ${test_index_0} <= 3 ... \\ Log to Console Index: {{wlv}} ... \\ Increment +1 Log to Console Testing 3 >= 0 Loop WHILE${test_index_2} >= 0
...     \\  Log to Console  Index: {{wlv}}
...     \\  Increment   -1
Log to Console  Final
Log to Console  Testing 3 != 0
Loop    WHILE   \${test_index_2} != 0
...     \\  Log to Console  Index: {{wlv}}
...     \\  Increment   -1
`