3
\$\begingroup\$

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
asked Jul 26, 2017 at 16:20
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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
answered Nov 25, 2017 at 0:11
\$\endgroup\$
2
  • \$\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\$ Commented 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\$ Commented Mar 28, 2020 at 17:34

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.