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
\$\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
\$\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

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.