How Robot Framework evaluates keywords with embedded arguments

Let's look at how Robot Framework takes a statement like this:

Add 3 and 4

And turns it into a function call like this:

add('1', '2')

Take an Arithmetic library that provides a few keywords with embedded arguments:

In [1]:
from robot.api import deco

class Arithmetic:
    
    @deco.keyword("Add ${first} and ${second}")
    def add(self, first: str, second: str) -> float:
        return float(first) + float(second)

    @deco.keyword("Subtract ${first} from ${second}")
    def subtract(self, first: str, second: str) -> float:
        return float(second) - float(first)
    
    @deco.keyword("Multiply ${first} and ${second}")
    def multiply(self, first: str, second: str) -> float:
        return float(second) - float(first)

At the beginnning of a test case, Robot Framework creates an instance of this class:

In [2]:
arithmetic = Arithmetic()

Robot Framework adds a robot_name attribute to keywords with embedded arguments:

In [3]:
print(arithmetic.add.robot_name)
Add ${first} and ${second}

We can do a little bit of magic with the inspect library to find all methods in the Arithmetic class that have a robot_name attribute. We'll store the text of the keyword and the function associated with the keyword in a namedtuple so that we can retrieve them later.

In [4]:
import inspect
from collections import namedtuple

Keyword = namedtuple("Keyword", ["name", "method"])

methods = [
    i[1] for i in inspect.getmembers(arithmetic, inspect.ismethod)
]

keywords = [
    Keyword(getattr(method, "robot_name"), method)
    for method in methods
    if hasattr(method, "robot_name")
]

for keyword in keywords:
    print(f"Name: {keyword.name}")
    print(f"Method: {keyword.method.__name__}\n")
Name: Add ${first} and ${second}
Method: add

Name: Multiply ${first} and ${second}
Method: multiply

Name: Subtract ${first} from ${second}
Method: subtract

Now, given a string like:

Add 1 and 2

How do we match that to one of our keywords? We can substitute all instances of Robot Framework's variable syntax (that is, everything around a ${}) with a (.*) to create a regular expression that matches any uses of the keyword. The .* matches any string in the variable's place and the () creates a capturing group that we can use to retrieve the values being passed as arguments.

In [5]:
import re

regex_keywords = []

for keyword in keywords:
    # substitute all instances of ${} with (.*)
    regex_name = re.sub("\$\{.+?\}", "(.*)", keyword.name)
    print(regex_name)
    
    regex_keyword = Keyword(regex_name, keyword.method)
    regex_keywords.append(regex_keyword)
Add (.*) and (.*)
Multiply (.*) and (.*)
Subtract (.*) from (.*)

Given a string like:

Add 1 and 2

We can check for a match against each regular expression.

In [6]:
keyword_str = "Add 1 and 2"

matching_keywords = [
    keyword
    for keyword in regex_keywords
    if re.fullmatch(keyword.name, keyword_str)
]

matching_keyword = matching_keywords[0]

print("Found regular expression match:")
print(matching_keywords[0].name)
Found regular expression match:
Add (.*) and (.*)

Now that we have a match, all that's left is to pull the values 1 and 2 out of the keyword so that we can call the matching function. We've already captured them as regex groups which can be retrieved from Match.groups().

In [7]:
match = re.fullmatch(matching_keyword.name, keyword_str)
keyword_params = match.groups()
print(keyword_params)
('1', '2')

And all that's left is to unpack these as parameters to the matching function!

In [8]:
matching_keyword.method(*keyword_params)
Out[8]:
3.0