Purpose: To allow nested For Loops with a minimum of syntax change from the standard For Loop.
Reason: I want to nest For Loops in Robot Framework. The code is working perfectly as a custom library/keyword in Robot Framework, but I'm asking for input to improve the code. I have no formal training in Python. This script was how I learned the language, so please don't expect it to be "Pythonic", I think the term is.
Sample Robot Framework code to run this keyword:
*** Settings ***
Library ExtendedSelenium2Library
Library Collections
Library Loops.py
*** Variables ***
${gold_squadron} = Gold
${red_squadron} = Red
*** Test Cases ***
Test For Loop
For Loop IN RANGE 0 1 INDEX0
... \\ For Loop IN RANGE 1 6 INDEX1
... \\ \\ Assign Internal Variable {{standing_by}} Standing By Red Leader
... \\ \\ Run Keyword If INDEX1 == 1 Log to Console ${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
... \\ \\ Assign Internal Variable {{standing_by_2}} Standing By Gold Leader
... \\ \\ Run Keyword If INDEX2 == 1 Log to Console ${gold_squadron} Leader Standing By
... \\ \\ Run Keyword Unless INDEX2 == 1 Log to Console ${gold_squadron} INDEX2 {{standing_by_2}}
And here is the code I'm working on.
from robot.libraries.BuiltIn import BuiltIn
# TODO: Create new types of For Loops and 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 = {}
# 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:
BuiltIn().run_keyword(*keyword_set)
# If it's a multiple-keyword list, then I have to use Run Keywords
else:
BuiltIn().run_keywords(*keyword_set)
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
# For each argument
for x in keywords:
# Format it to a string
x = str(x)
# If the keyword in question happens to be one of the 'Assign Internal Variable' keywords, then I need
# to run it now, not later.
# By splitting it up, I add a little complexity to the code but speed up execution when you're just
# assigning a scalar variable as opposed to having to search through the next few items just to find
# what I know is just going to be the next one.
# So, if it's the simple assignment...
if x.lower() == 'assign internal variable':
# ...run the Assign Internal Variable keyword with the two inputs
BuiltIn().run_keyword(x, *keywords[int(index)+1:int(index)+3])
# If it's the more complicated variable...
elif x.lower() == 'assign internal variable to keyword':
# ...initialize variables...
deliminator_search = 0
k_check = x
# ...search the next few keywords for a deliminator...
while k_check != '\\' and k_check != '\\\\':
deliminator_search = deliminator_search + 1
k_check = keywords[int(index)+deliminator_search]
# ...and run the Assign Internal Variable to Keyword keyword with the found keyword
BuiltIn().run_keyword(x, *keywords[int(index)+1:int(index)+2+deliminator_search])
# 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] = '\\\\'
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
@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
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
# The syntax for an internal variable is '{{varName}}' where varName can be anything
self.internal_variables[variable_name] = assignment
def assign_internal_variable_to_keyword(self, variable_name, keyword, *assignment):
# This keyword works like any other keyword so that it can be activated by BuiltIn.run_keywords
# The syntax for an internal variable is '{{varName}}' where varName can be anything
self.internal_variables[variable_name] = BuiltIn.run_keyword(keyword, *assignment)
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...
if keyword.lower() == 'assign internal variable'\
and keyword.lower() != 'assign internal variable to keyword':
# ...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
return keywords # Return the list of keywords to be used in the format loop
1 Answer 1
I found my shortcuts and performance enhancements via list comprehension.
Test Code:
*** Settings ***
Library Loops.py
*** Variables ***
${blue_squadron} = Blue
${gold_squadron} = Gold
${green_squadron} = Green
${red_squadron} = Red
*** 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
*** Keywords ***
Get Blue Squadron
[Return] standing by
Get Green Squadron
[Arguments] ${text}
[Return] standing by
Source Code:
from robot.libraries.BuiltIn import BuiltIn
class Loops(object):
def __init__(self):
self.internal_variables = {}
def for_loop(self, loop_type, start, end, index_var, *keywords):
keywords = self._format_loop(*keywords) # Format the keywords
self.internal_variables = {} # Clean out internal variables
for loop_iteration in range(int(start), int(end)): # The actual looping part
keyword_set = [k.replace(index_var, str(loop_iteration)) for k in keywords] # Replace the index_var
temp = self._run_keywords(*keyword_set) # Run the keywords with this iteration.
if not temp:
break
@staticmethod
def _format_loop(*keywords):
keywords = list(keywords) # I need to format the keywords as a list.
del_list = [i for i, x, in enumerate(keywords) if x == '\\'
and keywords[i - 1] == '\\' or i == 0] # Delete indices on this list
swap_list = [i for i, x, in enumerate(keywords) if x == '\\'
and (keywords[i - 1] != '\\' and keywords[i + 1] == '\\' and i != 0)] # Swap indices on this list
keywords = [k for i, k, in enumerate(['\\\\' if i in swap_list else x for i, x in enumerate(keywords)]
) if i not in del_list] # Actually does the swapping/deleting.
return keywords # Return the formatted list of keywords
def _run_keywords(self, *key_list):
keys = [-1] + [i for i, k in enumerate(key_list) if k == '\\'] + [len(key_list)] # Find the deliminators
for i, d in list(enumerate(keys[:-1])): # For each deliminator...
key_name = key_list[d + 1] # Get the name of the keyword
key_args = key_list[d + 2:keys[i + 1]] # Get the arguments for the keyword
if str(key_name)[-2:] == ' =': # If it's an internal variable, assign it.
self._assign_internal_variable(key_name[:-2], *key_args)
elif str(key_name)[-1:] == '=': # If it's the other version, assign it.
self._assign_internal_variable(key_name[:-1], *key_args)
else: # Otherwise, run the keyword
for k in self.internal_variables.keys():
key_args = [x.replace(k, self.internal_variables[k]) for x in key_args]
BuiltIn().run_keyword(key_name, *key_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):
self._assign_internal_variable(variable_name, BuiltIn().run_keyword(assignment, *arguments))
else:
self.internal_variables[variable_name] = assignment
-
\$\begingroup\$ Thank you for this post, I am working on other control statement based on this. Also how about adding continue and exit support for each loop? \$\endgroup\$Sariq Shaikh– Sariq Shaikh2020年03月28日 10:27:50 +00:00Commented Mar 28, 2020 at 10:27
-
\$\begingroup\$ I have not been working in Robot Framework for around two years now, and these are based in Python 2, so my opinion is out of date and practice in this case. My guess would be another option under "_run_keywords" in the if/elif/else statement to detect a certain keyword to trigger continue and exit, but if I remember anything from this script it's that nothing was ever simple and every new feature required a near-complete re-write, so please don't take that as a certainty. \$\endgroup\$Brandon Olson– Brandon Olson2020年03月28日 17:34:42 +00:00Commented Mar 28, 2020 at 17:34