Tset-Noitamotua / Sikuli-and-Robot-Framework-Integration

Windows 7 / 8.1 + Sikuli 1.1.0 compatible version of source for the great tuturial from Mike´s blog
http://blog.mykhailo.com/2011/02/how-to-sikuli-and-robot-framework.html
10 stars 5 forks source link

How to pass *args with a default value #9

Open Tset-Noitamotua opened 9 years ago

Tset-Noitamotua commented 9 years ago

This is not a SikuliX specific question - just a programming question of a beginner :)

I have a function / method like this:

def do(*args):
    # timeout value
    print "timeout is %i" % args[3]

    # action can be click, doubleclick, rightclick
    action = args[0]
    if args[0] == 'click':
        action = click
    elif args[0] == 'doubleclick':
        action = doubleClick
    elif args[0] == 'rightclick':
        action = rightClick

    # perform action e.g.
    # click image
    action((args[1]))

    # check post condition --> (args[2]) until timeout --> args[3])
    if exists((args[2]), (args[3])):
        pass
    else:
        print "FAIL! Wrong post condition!"

And I call it with that:

#  args[0] -> action | args[1] -> image to click | args[2] -> image to check
#  condition after action | args[3] -> timeout for exists()
do('click', "path_to/image_of_gui_element.png", "path_to/image_of_post_condition.png", 5)

Now my question: How can I set timeout argument agrs[3] to a default value (of e.g. 5 seconds) so that I don´t have to write it each time I call my do function?

RaiMan commented 9 years ago

Since you are using positional parameters (the position in the list defines its meaning), you can simply check the length of args: if len(args) < 4: timeout = 5 else: timeout = args[3]

Another possibility is to use a combination of fixed positional, variable positional and variable keywordargs for robustness (existence of fixed positionals are checked by the interpreter). a nice post: https://freepythontips.wordpress.com/2013/08/04/args-and-kwargs-in-python-explained/

Generally: when using these techniques, you should always first check the given parameters and assign the anonymous parameters to variables with speaking names

something like this to make it flexible and robust allows, to change only the parameter handling later, without touching the rest

def do(*args):
    # check for valid parameters
    argsLen = len(args)
    if argsLen == 0 or argsLen < 3 or not args[2]:
        raise "do: wrong parameters"

   # store given parameters
    actionName = args[0]
    clickTarget = args[1]
    postCondition = arg[2]
    if argsLen < 4: timeout = 5
    else: timeout = args[3]

    # log the given parameters
    print "do: %s(%s)" + ("" if not postCondition else "and check with exists(%s, %f)" %
                    (actionName, clickTarget, postCondition, timeout)

    # eval action click (default), double-click or rightclick
    if actionName.startsWith('d'):
        action = doubleClick
        actionName = "doubleClick"
    elif actionName .startsWith('r'):
        action = rightClick
        actionName = "doubleClick"
    else:
        action = click # default
        actionName = "click"

    # check clickTarget (Image or Pattern), can not be None (checked before)
    if not isinstance(clickTarget, string) and not is instance(clickTarget, Pattern):
        raise "do: clickTarget must be String or Pattern"

    # check postCondition (Image or Pattern), might be None
    if postCondition and not isinstance(postCondition, string) and not is instance(postCondition, Pattern):
        raise "do: postCondition must be String or Pattern"

    # perform action e.g.
    # click clickTarget
    action(clickTarget)

    if not postCondition:
        return

    # check postCondition until timeout 
    if not exists(postCondition, timeout):
        print "FAIL! Post condition not met!"
Tset-Noitamotua commented 9 years ago

Thank you very much, Raiman!

After nearly two week of diving in _args & *_kwargs I have the following which is working so far:

I have a class with a __init__() funciton which holds all parameters I need for my actions:

def __init__(self, **kwargs):
        self.timeout = kwargs.setdefault('timeout', 5.0)
        self.action = kwargs.setdefault('action', 'click')
        self.click_target = kwargs.setdefault('click_target', None)
        self.post_condition = kwargs.setdefault('post_condition', None)
        self.similarity = kwargs.setdefault('similarity', 0.7)
        Settings.MinSimilarity = self.similarity
        self.reg = Region(Screen())

Further I have some functions for the different types of actions the user can do, like click, double click etc. The function names also act as keyword names for the Robot Framework. For the moment they don´t do more than taking the given parameters which are passed as arguments from Robot Framework keywords. The given arguments are then handled by my do() function. (But this is a design question for know - maybe I will get rid of the do() function in the future.)

The different action functions look like this:

def click(self, *args, **kwargs):
        """
        Left mouse click on a click target (e.g. gui element)
        """

        self.action = 'click'

        # pass given arguments to -> do() function
        self.do(*args, **kwargs)

A custom Robot Framework keyword then can look like this:

*** Keywords ***
My Custom Keyword
    click    target.png
    click    target.png    post-condition.png
    click    target.png    post-condition.png   timeout=10.0
    click    target.png    post-conditon.png    similarity=0.5
    click    target.png    post-condition.png   timeout=11.0    similarity=0.55

Another Custom Keyword
    type    textstring
    type    textstring    post-condition.png    # optional   timeout=11   similarity=0.5
    ...

The do() function handles the given arguments:

 def do(self, *args, **kwargs):
        """
        Interacts with gui elements and checks condition after action.
        """

        # Check for valid arguments - - - - - - - - - - - - - - - - -  - - - - - - - - - - -
        if len(args) == 0 or not args[0]:
            raise ValueError('At least one argument (click target) is required')

        # If only one argument given
        elif len(args) < 2:
            # Store it with meaningful name/key
            self.click_target = args[0]

            # Check click_target, can not be None.
            if not isinstance(self.click_target, basestring) and \
                    not isinstance(self.click_target, Pattern):
                raise ValueError('First argument (click target) must be \
                String or Pattern!')

        # If more than one argument given
        else:
            # Store given arguments with meaningful names/keys
            self.click_target = args[0]
            self.post_condition = args[1]

            # Check click_target argument, can not be None.
            if not isinstance(self.click_target, basestring) and \
                    not isinstance(self.click_target, Pattern):
                raise ValueError('First argument (click target) must be \
                String or Pattern!')

            # check post_condition argument, might be None
            if not isinstance(self.post_condition, basestring) and \
                    not isinstance(self.post_condition, Pattern):
                raise ValueError('Second argument (post_condition) must be \
                String or Patter!')

        # first eval action then perform it - - - - - - - - - - - - - - - - - - - - - - - - - 
        if self.action == 'double_click':
            pass # TODO

        elif self.action == 'context_click':
            action = self.reg.rightClick

            # perform context_click action
            action(self.click_target)

            # check post_condition until timeout
            self.check_post_condition(*args, **kwargs)

            # restore default region
            self.reg = Screen()

        elif self.action == 'click':
            action = self.reg.click

            # perform click action
            action(self.click_target)

            # check post condition until timeout
            self.check_post_condition(*args, **kwargs)

            # restore default region
            self.reg = Screen()

        elif self.action == 'type':
            action = self.reg.type

            # perform action type
            action(self.click_target)

            # check post condition until timeout
            self.check_post_condition(*args, **kwargs)

            # restore default region
            self.reg = Screen()

        else:
            # befor this can happen RF will throw a 'unknown keyword' error
            raise ValueError('Unknown action "%s"!' % self.action)

Post condition after each action is checked in a seperate function:

def check_post_condition(self, *args, **kwargs):

        self.timeout = kwargs.get('timeout', 5.0)

        # change MinSimilarity if necessary
        self.similarity = kwargs.get('similarity', 0.7)
        Settings.MinSimilarity = float(self.similarity)

        # log given arguments
        self.log_arguments('CHECK_POST_CONDITION', *args, **kwargs)

        if not self.post_condition:
            self.log.passed('Action successfully performed!')
        else:
            # check post condition until timeout
            if not exists(self.post_condition, float(self.timeout)):
                self.log.failed('Post condition (%s) NOT MET!' % self.post_condition)
            else:
                self.log.passed('Expected post condition (%s) met.' % self.post_condition)

            # restore default post_condition
            self.post_condition = kwargs.get('post_condition', None)

            # restore default similarity
            Settings.MinSimilarity = 0.7
            self.similarity = Settings.MinSimilarity

For logging the given arguments I use a seperate function:

def log_arguments(self, in_func, *args, **kwargs):
        """ - - - - - - - - - - - - - - - - - - - - - -
          Logs all arguments that are passed from  RF
        - - - - - - - - - - - - - - - - - - - - - - - -
        """

        # log given *args
        print "*args in '%s()':" % in_func
        if args is not None:
            i = 0
            for arg in args:
                print "- args[%i] = %s" % (i, arg)
                i += 1
        print '\n'

        # log given **kwargs
        if kwargs is not None:
            print "**kwargs in '%s()': " % in_func
            for key, value in kwargs.iteritems():
                print "- kwargs: %s = %s" % (key, value)
        print '\n'

        # log __init__ vars
        print "__init__(vars) in '%s()':" % in_func
        print '- self.timeout:\t\t\t', self.timeout
        print '- self.action:\t\t\t', self.action
        print '- self.click_target:\t\t', self.click_target
        print '- self.post_condition:\t\t', self.post_condition
        print '- self.similarity:\t\t', self.similarity
        print '- self.press_key:\t\t', self.press_key
        print '\n'

Combined with Mikes´s tutorial this can become a realy cool Robot Framework Sikuli Library ;-)))

RaiMan commented 9 years ago

Interesting - looks good.

I just made a similar approach (varargs) to support running scripts and snippets written in JavaScript (the engine is contained in Java, so no external interpreter is needed).

This will become an additional generic API in version 2, to access the SikuliX features from anywhere.

keep up with your approach. welcome for any help you need.