Ed Blake avatar Ed Blake committed c333ba2 Draft

Initial release!

Comments (0)

Files changed (13)

+syntax: glob
+*.pyc
+*.pyo
+*~
+*.o
+*.egg-info
+*.dll
+*.swp
+*.pkl
+*.orig
+*.komodo*
+.DS_Store
+build
+dist
+reference
+reference/**
+doc/_build
+doc/_build/**
+MANIFEST
+.komodo*
+*.log
+*.csv
+*.scr
+*.bat
+*.vpv
+*.txt
+*.xls*
+*.un~
+*.blend*
+build/**
+PYAHK AutoHotKey via Python
+===========================
+:Description: Python ctypes wrapper around AutoHotKey dll.
+:License: BSD-style, see `LICENSES.txt`_
+:Author: Ed Blake <kitsu.eb@gmail.com>
+:Date: Aug. 30 2012
+:Revision: 1
+
+Introduction
+------------
+`AutoHotKey <http://www.autohotkey.com/>`_ is a powerful task automator with a
+terrible scripting language built-in. Python is a powerful scripting language
+with old, unmaintained, and incomplete automation modules. Combining the two
+creates a powerful automation tool with a powerful scripting back-end with 
+access to all the power of the Python standard library.
+
+This is made possible by the amazing Python ctypes module and the
+AutoHotKey.dll_ project. Together they allow exchange of data between both
+scripting engines, and the execution of AHK commands from Python.
+
+Requirements
+------------
+* Written for Python2.7_, not currently py3k compatible.
+* Written against AutoHotkey_H ANSI 32-bit Version 1.1.8.1 (on WinXP).
+* A copy of the ANSI 32-bit dll must be provided either in the system location
+  of your version of Windows, or in the same folder as you import ahk.
+  (The required dll is not provided as part of this distribution, see the
+  AutoHotKey.dll_ site for download instructions.)
+
+Installation
+------------
+Currently only: `python setup.py install`
+
+Testing
+-------
+A helper script "runtests.py" is provided in the project root to run the entire 
+test suite. Runnable test scripts are provided for each sub-module in the test 
+folder. Test coverage is a little light, see ToDo_ list.
+
+Usage
+-----
+First import the ahk module::
+
+    import ahk
+
+Lower level function wrappers provide more direct access to the underlaying dll::
+
+    ahk.start() # Ititializes a new script thread
+    ahk.ready() # Waits until status is True
+    ahk.execute('a := WinActive("Notepad")') # Sets a to the return value of WinActive
+    print get('a') # prints the HWND of the found window or 0x0 as a string
+
+Object wrappers are also provided which have a somewhat cleaner interface::
+
+    script = ahk.Script() # Initializes with start and ready commands as above
+    a = script.winActive("Notepad") # Sets a to the return value of the method
+    print a # prints the HWND of the found window as an int, or None
+    script.variable('b', float) # Creates a transparent variable attribute
+    ahk.execute("Random, b, 0.0, 1.0") # Stores value in `b`
+    print 100*script.b # b is retrieved and converted to its saved type (float)
+
+See the Docs_ for further details.
+
+ToDo
+----
+* Add remaining Control commands to Control class.
+* Add doc-tests.
+* Migrate docs to `ReadTheDocs <http://read-the-docs.readthedocs.org/en/latest/getting_started.html>`_?
+* Extend Script class (ahk.execute is annoying).
+* Debug AHK function calling wrappers (can't use arguments).
+* Figure out if Scripts can have their own thread and context.
+* Finish writing low-level tests.
+* Mock out dll so high-level tests can be written (i.e. for winActive et al).
+* Add examples directory.
+* Add optional unicode support.
+
+.. _LICENSES.txt: https://bitbucket.org/kitsu/pyahk/src/tip/LICENSES.txt
+.. _AutoHotKey.dll: http://www.autohotkey.net/~HotKeyIt/AutoHotkey/files/AutoHotkey-dll-txt.html
+.. _Python2.7: http://python.org/download/releases/2.7.3/#download 
+.. _Docs: http://kitsu_eb.pythonanywhere.com/docs/pyahk
+"""OO wrapper around The AutoHotKey library.
+
+This module provides both direct access to wrapped versions of the functions
+provided by the ahkdll, and also object wrappers around common operations.
+"""
+import ctypes, time, os, sys
+
+# This try/except allows documentation to be generated without access to the dll
+try:
+    _ahk = ctypes.cdll.AutoHotkey #load AutoHotKey dll
+except (WindowsError, OSError):
+    print "Warning: Can't load AutoHotKey.dll, all ahk functions will fail."
+
+__version__ = "0.1.0"
+
+class Function(object):
+    """Object wrapper around ahk functions"""
+    template = "\n{0}{1} {{\n{2}\n}}"
+
+    def __init__(self, name, result, args, body):
+        """
+        Called Functions are automatically transform to their result by calling
+        the provided result function on the return value from the ahk function
+        call.
+
+        :param name: The name of the function to wrap.
+        :type name: str
+        :param result: Type of the expected result.
+        :type result: callable type (default=str)
+        :param args: The argument definition of the function.
+        :type args: str (default='()')
+        :param body: The body of the function (excluding braces).
+        :type body: str (default='')
+        """
+        self.name = name
+        self.definition = self.template.format(name, args, body)
+        execute(self.definition)
+        self.result = result
+        # Because the ahkdll function call function is broken use temp
+        # variables as a work-around.
+        self.tmpname = "tmp{0}".format(str(int(time.time())))
+
+    def __call__(self, *args):
+        """Call the wrapped function and return the converted result."""
+        args = ",".join(str(i) for i in args)
+        # Execute the function and assign the result to the temp variable
+        execute("{0} := {1}({2})".format(
+            self.tmpname, self.name, args))
+        # Get the result from the temp variable
+        result = self.result(get(self.tmpname))
+        # And delete the temp variable value
+        set(self.tmpname, "")
+        return result
+
+class Script(object):
+    """Wrapper around ahk script commands."""
+
+    def __init__(self, script="", filename=None):
+        """
+        Initializes the ahk script engine just like calling the low-level
+        function ahk.start followed by ahk.ready.
+        """
+        self._handle = start(script=script, filename=filename)
+        ready()
+        self._vars = dict()
+        self._funcs = dict()
+        self._tmpname = "tmp{0}".format(str(int(time.time())))
+
+    def __del__(self):
+        """Call terminate to kill the script engine."""
+        terminate()
+
+    def variable(self, name, kind=str, value=''):
+        """Create a new ahk variable wrapper.
+
+        Wrapped variables are tracked by the instance and can be accessed as
+        object attributes. Variables are automatically transformed to their
+        declared kind by calling their kind with their ahk string value as the
+        first argument (just like normal python type conversions).
+
+        .. note::
+            Variables are not allowed to start with '_' the underscore char!
+        
+        :param name: Then name of the new variable.
+        :type name: str
+        :param kind: Type of the variable (cast on get).
+        :type kind: callable type (default=str)
+        :param value: Initial value of the variable.
+        :type value: match kind or str (default='')
+        :raises: AttributeError if the provided name already exists in the
+            instance as either a variable or an attribute.
+        """
+        if name[0] == '_':
+            raise AttributeError(
+                    "Variable names may not start with an underscore!")
+        if hasattr(self, name):
+            raise AttributeError(
+                    "Name: {0} already exists as an attribute!".format(name))
+        if name in self._vars:
+            raise AttributeError(
+                    "Name: {0} already exists as a variable!".format(name))
+        if name in self._funcs:
+            raise AttributeError(
+                    "Name: {0} already exists as a function!".format(name))
+
+        if set(name, value):
+            self._vars[name] = kind
+        else:
+            raise AttributeError(
+                    "Failure reported by ahk while setting {0}={1}!".format(
+                        name, value))
+
+    def function(self, name, result=str, args='()', body=''):
+        """Create a new ahk function wrapper.
+        
+        Wrapped functions are tracked by the instance and can be accessed as
+        object attributes. Functions are automatically transform to their
+        result by calling the provided result function on the return value from
+        the ahk function call.
+
+        .. note::
+            Function names are not allowed to start with '_' the underscore char!
+
+        :param name: The name of the function to wrap.
+        :type name: str
+        :param result: Type of the expected result.
+        :type result: callable type (default=str)
+        :param args: The argument definition of the function.
+        :type args: str (default='()')
+        :param body: The body of the function (excluding braces).
+        :type body: str (default='')
+        :raises: AttributeError if the indicated name is already used.
+        :returns: Function wrapper object.
+        """
+        if name[0] == '_':
+            raise AttributeError(
+                    "Function names may not start with an underscore!")
+        if hasattr(self, name):
+            raise AttributeError(
+                    "Name: {0} already exists as an attribute!".format(name))
+        if name in self._funcs:
+            raise AttributeError(
+                    "Name: {0} already exists as a function!".format(name))
+        if name in self._vars:
+            raise AttributeError(
+                    "Name: {0} already exists as a variable!".format(name))
+
+        func = Function(name, result, args, body)
+        self._funcs[name] = func
+        return func
+
+    def send(self, keys, mode='SendInput'):
+        """Convenience wrapper to send input to the active window.
+        
+        Sends a ahk formatted series of keystrokes to the active window.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/Send.htm>`_
+        for more details.
+
+        :param keys: The keys to be sent.
+        :type keys: str
+        :param mode: The ahk command used to send keys. Valid modes:
+
+            * Send
+            * SendRaw
+            * SendInput
+            * SendPlay
+            * SendEvent
+
+        :type mode: str
+        """
+        execute("{0} {1}".format(mode, keys))
+
+    def click(self, button="", count=1, x=None, y=None):
+        """Convenience wrapper to the ahk click function.
+
+        Send a mouse click of any type to any coordinate with optional repeats.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/Click.htm>`_
+        for more details.
+        The button argument can actually take a number of options:
+
+            * `blank` - Simple primary click.
+            * right - Auxiliary button click.
+            * wheelup - Send mouse wheel scroll event.
+            * down - Press but don't release the primary button.
+            * rel[ative] - Interpret coordinates in relative mode.
+            * etc.
+
+        Button options can be composed when not mutually exclusive 
+        (e.g. "right down relative").
+
+        Use count=0 with x and y to move the mouse without clicking.
+
+        :param button: The mouse button(s) to click.
+        :type button: str (default="")
+        :param count: Number of times to repeat the click.
+        :type count: int (default 1)
+        :param x: Mouse x-coord.
+        :type x: int
+        :param y: Mouse y-coord.
+        :type y: int
+        """
+        cmd = "Click {0}".format(button)
+        if x or y: # Could fail for x&y = 0
+            cmd += "{0},{1} ".format(x, y)
+        # Click count must occur "somewhere right of the coordinates"
+        if count != 1:
+            cmd += "{0} ".format(count)
+        execute(cmd+'\n')
+
+    def winActivate(self, title="", text="", 
+                    extitle="", extext="", bottom=False):
+        """Convenience wrapper for ahk WinActivate commands.
+        
+        Used to set a window with provided parameters as active.
+        If all parameters are empty the `last found` window is activated.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/WinActivate.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :param bottom: Whether to act on the bottom-most window.
+        :type bottom: bool (default=False)
+        """
+        cmd = "WinActivate"
+        if bottom:
+            cmd = "WinActivateBottom"
+        execute('{0} := {1}("{2}", "{3}", "{4}", "{5}")'.format(
+            self._tmpname, cmd, title, text, extitle, extext))
+
+    def winActive(self, title="", text="", extitle="", extext=""):
+        """Convenience wrapper for ahk IfWinActive command.
+        
+        Used to check if a window with provided parameters is active.
+        If all parameters are empty the `last found` window is checked.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/IfWinActive.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :returns: The found window's HWND or None.
+        """
+        set(self._tmpname, '')
+        execute('{0} := WinActive("{1}", "{2}", "{3}", "{4}")'.format(
+            self._tmpname, title, text, extitle, extext))
+        result = int(get(self._tmpname), 0)
+        if result == 0:
+            return None
+        return result
+
+    def winExist(self, title="", text="", extitle="", extext=""):
+        """Convenience wrapper for ahk IfWinExist command.
+        
+        Used to check if a window with provided parameters exists.
+        If all parameters are empty the `last found` window is checked.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/IfWinExist.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :returns: The found window's HWND or None.
+        """
+        set(self._tmpname, '')
+        execute('{0} := WinExist("{1}", "{2}", "{3}", "{4}")'.format(
+            self._tmpname, title, text, extitle, extext))
+        result = int(get(self._tmpname), 0)
+        if result == 0:
+            return None
+        return result
+
+    def waitActive(self, title="", text="", timeout=5, 
+                   extitle="", extext="", deactivate=False):
+        """Convenience wrapper for ahk WinWaitActive command.
+        
+        Used to wait until a window matching the given parameters is activated.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/WinWaitActive.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param timeout: How long in seconds to wait for the window to activate.
+            Use None to wait indefinitely.
+        :type timeout: int or None (default=5)
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :param deactivate: Toggle between WinWaitActive and WinWaitNotActive.
+        :type deactivate: bool (default=False)
+        :returns: The True if matching window is activated, else False.
+        """
+        cmd = "WinWaitActive"
+        if deactivate:
+            cmd = "WinWaitNotActive"
+        if timeout is None:
+            timeout = ""
+        execute("{0}, {1}, {2}, {3}, {4}, {5}".format(
+                cmd, title, text, timeout, extitle, extext))
+        result = int(get("ErrorLevel"), 0)
+        if result != 0:
+            return False
+        return True
+
+    def waitWindow(self, title="", text="", timeout=5, 
+                   extitle="", extext="", closed=False):
+        """Convenience wrapper for ahk WinWait command.
+        
+        Used to wait until a window matching the given parameters exists.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/WinWait.htm>`_,
+        `AHK docs <http://www.autohotkey.com/docs/commands/WinWaitClose.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param timeout: How long in seconds to wait for the window to activate.
+            Use None to wait indefinitely.
+        :type timeout: int or None (default=5)
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :param closed: Toggle between WinWait and WinWaitClose.
+        :type closed: bool (default=False)
+        :returns: The True if matching window is activated, else False.
+        """
+        cmd = "WinWait"
+        if closed:
+            cmd = "WinWaitClose"
+        if timeout is None:
+            timeout = ""
+        execute("{0}, {1}, {2}, {3}, {4}, {5}".format(
+                cmd, title, text, timeout, extitle, extext))
+        result = int(get("ErrorLevel"), 0)
+        if result != 0:
+            return False
+        return True
+
+    def __getattr__(self, name):
+        """Override attribute lookup to add ahk variable access."""
+        #NOTE __getattr__ is only called on attrs that aren't found normally
+        if name in self._vars:
+            return self._vars[name](get(name))
+        elif name in self._funcs:
+            return self._funcs[name]
+        else:
+            raise AttributeError("No variable named {0}!".format(name))
+
+    def __setattr__(self, name, value):
+        """Override attribute modification to add ahk variable access."""
+        # Also filter names starting with '_' (special names).
+        if name[0] == '_' or name in self.__dict__:
+            super(Script, self).__setattr__(name, value)
+        elif name in self._vars:
+            set(name, value)
+        elif name in self._funcs:
+            raise AttributeError("Can't assign to function {0}!".format(name))
+        else:
+            raise AttributeError("No variable named {0}!".format(name))
+
+class Control(object):
+    """Wrapper around ahk window control commands."""
+
+    def __init__(self, script, title="", text="", extitle="", extext="", 
+                 store=True):
+        """Initialize a new Control object.
+        
+        Stores information about the window who's controls you will manipulate.
+
+        :param script: Script object to use internally.
+        :type script: ahk.Script instance
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :param store: Whether to cache the found window.
+        :type store: bool
+        :raises: NameError if store=True but a matching window can't be found.
+        """
+        self.script = script
+        self.params = (title, text, extitle, extext)
+        self.hwnd = None
+        if store:
+            self.hwnd = script.winExist(title, text, extitle, extext)
+            if not self.hwnd:
+                win = "win(title={0}, text={1}, extitle={2}, extext={3})"
+                raise NameError("Can't find matching window:\n\t" + 
+                                win.format(*self.params))
+
+    def click(self, control="", pos=None, button="", count=1, options=""):
+        """Sends mouse input to a control.
+        
+        The control arg can be a class name, window handle, or text string.
+        Alternately pos as a (x, y) tuple may be provided indicating the mouse
+        coordinates relative to the top-left corner of the application window
+        in which to click. control and pos are mutually exclusive.
+
+        Default is a single left click, but you can send left/right/middle,
+        wheel up or down left or right with a repeat count.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/ControlClick.htm>`_
+        for more details.
+
+        :param control: The class, HWND, or text of the control to click.
+        :type control: str
+        :param pos: The relative coordinates of at which to click.
+        :type pos: tuple (indexable)
+        :param button: The name of the button to be clicked.
+        :type button: str
+        :param count: Number of times it should be clicked.
+        :type count: int
+        :param options: Optional arguments.
+        :type options: str
+        """
+        if self.hwnd:
+            title = "ahk_id {0}".format(self.hwnd)
+            text, extitle, extext = "", "", ""
+        else:
+            title, text, extitle, extext = self.params
+
+        if pos:
+            # Position wins, overwrite control
+            control = "X{0[0]} Y{0[1]}".format(pos)
+            # Add pos option to force coordinate interpretation
+        # Setup window id args
+        cmd = "ControlClick, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}"
+        execute(cmd.format(control, title, text, button, count, 
+                           options, extitle, extext))
+
+    def send(self, control="", keys="", raw=False):
+        """Send keystrokes to a control.
+
+        See `AHK docs <http://www.autohotkey.com/docs/commands/ControlSend.htm>`_
+        for more details.
+
+        :param control: The class, HWND, or text of the control to click.
+        :type control: str
+        :param keys: Keystrokes in the same format as ahk.Script.send.
+        :type keys: str
+        :param raw: Toggle raw input mode.
+        :type raw: bool (default False)
+        """
+        if self.hwnd:
+            title = "ahk_id {0}".format(self.hwnd)
+            text, extitle, extext = "", "", ""
+        else:
+            title, text, extitle, extext = self.params
+
+        cmd = "ControlSend"
+        if raw:
+            cmd = "ControlSendRaw"
+        cmd = cmd + ", {0}, {1}, {2}, {3}, {4}" 
+        execute(cmd.format(control, keys, title, text, extitle, extext))
+
+# Wrapper functions ------------------------------------------------------------
+
+def start(filename=None, script="", options="", params=""):
+    """Wrapper around ahkdll and ahktextdll.
+    
+    Start a new ahk thread from file or a string.
+    Defaults to an empty script with no options or params.
+    Filename is preferred over script if provided.
+
+    .. Note::
+        Using any option besides ahk.start() seems not to work.
+        Specifying a file doesn't cause it to run, passing a string
+        doesn't cause it to be executed?
+
+    :returns: Thread handle for created instance (see thread functions).
+    """
+    #print filename
+    if filename:
+        return _ahk.ahkdll(os.path.abspath(filename), options, params)
+    else:
+        return _ahk.ahktextdll(script, options, params)
+
+def ready(nowait=False, retries=None):
+    """Wrapper around ahkReady.
+    
+    Returns True if ahk is ready to use.
+    By default this polls the dll function until it is ready.
+    By calling with nowait=True the immediate result is returned instead.
+    By calling with retries > 1 state will be checked at most retries times.
+    """
+    if nowait:
+        retries = 1
+    if retries and retries >= 1:
+        for i in range(retries):
+            if _ahk.ahkReady() == 1:
+                return True
+            time.sleep(0.01)
+    else:
+        while 1:
+            if _ahk.ahkReady() == 1:
+                return True
+            time.sleep(0.1)
+    return False
+
+def add_lines(script="", filename=None, duplicates=False, ignore=True):
+    """Wrapper around addFile and addScript.
+
+    Adds lines to the running script from file or string.
+    Lines added from string are evaluated immediately,
+    lines added from file are not evaluated.
+    Defaults to an empty script, no duplicates, and ignore errors.
+    Filename is preferred over script if provided.
+
+    :returns: Pointer address to first line in added script (see execute_line).
+    """
+    if filename:
+        if duplicates:
+            duplicates = 1
+        else:
+            duplicates = 0
+
+        if ignore:
+            if type(ignore) != int:
+                ignore = 1
+        else:
+            ignore = 0
+        return int(_ahk.addFile(os.path.abspath(filename), duplicates, ignore))
+    else:
+        return int(_ahk.addScript(script))
+
+def execute(script):
+    """Wrapper around ahkExec.
+
+    Execute provided ahk commands. No lines are added to the active script.
+
+    :returns: True if successful, else False.
+    """
+    result = _ahk.ahkExec(script)
+    if result == 1:
+        return True
+    return False
+
+def jump(label, nowait=False):
+    """Wrapper around ahkLabel.
+    
+    GoSub/GoTo like function, branch to labeled location.
+    Defaults to nowait=False, i.e. GoSub mode.
+    Using nowait=True, i.e. GoTo mode, is unreliable and may fail entirely.
+
+    :returns: True if label exists, else False.
+    """
+    if nowait:
+        nowait = 1
+    else:
+        nowait = 0
+
+    result = _ahk.ahkLabel(label, nowait)
+    if result == 1:
+        return True
+    return False
+
+def call(func, *args):
+    """Wrapper around ahkFunction.
+
+    Call the indicated function.
+
+    :returns: Result of function call as a string.
+    """
+    params = None
+    if args:
+        string_array = ctypes.c_char_p*len(args)
+        params = string_array(*[ctypes.c_char_p(str(arg)) for arg in args])
+    result = _ahk.ahkFunction(func, params)
+    return ctypes.cast(int(result), ctypes.c_char_p).value
+
+def post(func, *args):
+    """Wrapper around ahkPostFunction.
+
+    Call the indicated function but discard results.
+
+    :returns: True if function exists, else False.
+    """
+    params = None
+    if args:
+        string_array = ctypes.c_char_p*len(args)
+        params = string_array(*[ctypes.c_char_p(str(arg)) for arg in args])
+    result = _ahk.ahkPostFunction(func, params)
+    if result == 0: # 0 if function exists, else -1
+        return True
+    return False
+
+def set(name, value):
+    """Wrapper around ahkassign.
+    
+    Assigns the string `value` to the variable `name`.
+
+    :returns: True for success and False for failure.
+    """
+    if not type(value) in (str, unicode):
+        value = str(value)
+    result = _ahk.ahkassign(name, value)
+    if result == 0: # 0 for success, else -1
+        return True
+    return False
+
+def get(name, pointer=False):
+    """Wrapper around ahkgetvar.
+
+    Get the string value of a variable from ahk.
+    Call with pointer=True to request a reference to the variable.
+
+    :returns: A string representing the value, or a c_char_p.
+    """
+    # Workaround for ahkgetvar always returning pointers
+    result = _ahk.ahkgetvar(name, 0)
+    result = ctypes.cast(int(result), ctypes.c_char_p)
+    if pointer:
+        return result
+    return result.value
+
+def terminate(timeout=1):
+    """Wrapper around ahkTerminate.
+
+    Terminate the script, removing all hotkeys and hotstrings.
+    The default timeout is 1ms, must be positive > 0.
+    """
+    _ahk.ahkTerminate(timeout)
+
+def reload():
+    """Wrapper around ahkReload.
+
+    Terminates and restarts the script.
+    """
+    _ahk.ahkReload()
+
+def find_func(name):
+    """Wrapper around ahkFindFunc.
+
+    Get a pointer address to the named function.
+    To use this you must first create a ctypes CFUNCTION prototype::
+
+        proto = ctypes.CFUNCTION(ctypes.c_int, ctypes.c_char_p)
+
+    Then call the prototype with the address as an argument to get a function::
+
+        func = proto(address)
+
+    Now it can be called::
+
+        result = func(5)
+
+    :returns: The address of the function as an integer.
+    """
+    return int(_ahk.ahkFindFunc(name))
+
+def find_label(name):
+    """Wrapper around ahkFindLabel.
+
+    Get a pointer address to a label...
+
+    :returns: The address of the label as an integer.
+    """
+    return int(_ahk.ahkFindLabel(name))
+
+def pause(pause_=True):
+    """Wrapper around ahkPause.
+
+    Pause or unpause the script.
+    Calling with pause_=True pauses, pause_=False un-pauses. 
+    Calling with pause_=None just reports current state without changing it.
+
+    :returns: True if the script is paused, else False.
+    """
+    # Changed arg name from pause to pause_ to appease pylint:
+    # Redefining name 'pause' from outer scope
+    if pause_:
+        pause_ = 1
+    elif pause_ is None:
+        pause_ = ""
+    else:
+        pause_ = 0
+    result = _ahk.ahkPause(pause_)
+    if result == 1:
+        return True
+    return False
+
+def exec_line(line=None, mode=3, wait=False):
+    """Wrapper around ahkExecuteLine.
+
+    Execute starting from the provided line address.
+    If line=None the address of the first line will be returned.
+    Four modes of execution are available:
+
+        0. No execution, but the next line address is returned.
+        1. Run until a return statement is found.
+        2. Run until the end of the current block.
+        3. (default) execute only one line.
+
+    Setting wait=True will block until end of execution.
+
+    :returns: A line pointer address.
+    """
+    if not line:
+        return int(_ahk.ahkExecuteLine("", 3, 0))
+    elif wait:
+        wait = 1
+    else:
+        wait = 0
+    return int(_ahk.ahkExecuteLine(line, mode, wait))
+

doc/_templates/layout.html

+{% extends "!layout.html" %}
+{% block footer %}
+        {{ super() }}
+        <div class="footer">Served by <a href="http://bottlepy.org/docs/dev/">Bottle</a></div>
+{% endblock %}
+
+# -*- coding: utf-8 -*-
+#
+# pyahk documentation build configuration file, created by
+# sphinx-quickstart on Wed Aug 29 17:22:00 2012.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+sys.path.insert(0, os.path.abspath('../'))
+import ahk
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage']
+
+# -- AutoDoc settings ---------
+autoclass_content = 'both'
+autodoc_member_order = 'bysource'
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'pyahk'
+copyright = u'2012, Ed Blake'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.1.0'
+# The full version, including alpha/beta/rc tags.
+release = '0.1.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'pyahkdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'pyahk.tex', u'pyahk Documentation',
+   u'Ed Blake', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'pyahk', u'pyahk Documentation',
+     [u'Ed Blake'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+  ('index', 'pyahk', u'pyahk Documentation',
+   u'Ed Blake', 'pyahk', 'One line description of project.',
+   'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+.. include:: ../README.rst
+
+.. toctree::
+   :maxdepth: 1
+   :hidden:
+
+   wrappers
+   objects
+
+Indices and tables
+------------------
+
+* :ref:`genindex`
+* :ref:`search`
+
+Wrapper Objects
+===============
+These are OO wrappers around the lower level functions. They provide a simpler
+interface, but are meant to compliment the basic wrapper functions. The
+advantage of the object wrappers are separation from implementation details,
+automatic type conversions, and pre-written interfaces to ahk script commands
+not exposed by the dll file.
+
+classes
+-------
+   * :class:`.Function`
+   * :class:`.Script`
+       * :meth:`.Script.variable`
+       * :meth:`.Script.function`
+       * :meth:`.Script.send`
+       * :meth:`.Script.click`
+       * :meth:`.Script.winActivate`
+       * :meth:`.Script.winActive`
+       * :meth:`.Script.winExist`
+       * :meth:`.Script.waitActive`
+       * :meth:`.Script.waitWindow`
+   * :class:`.Control`
+       * :meth:`.Control.send`
+       * :meth:`.Control.click`
+
+Function
+^^^^^^^^
+.. autoclass:: ahk.Function
+    :members:
+
+Script
+^^^^^^
+.. autoclass:: ahk.Script
+    :members:
+
+Control
+^^^^^^^
+.. autoclass:: ahk.Control
+    :members:
+
+Function wrappers
+=================
+These are lower level wrappers around the raw dll functions. Functions have
+been combined and simplified where reasonable. Only the minimal
+extraction/conversion has been preformed on c return types.
+
+functions
+---------
+   * :func:`.start`
+   * :func:`.ready`
+   * :func:`.add_lines`
+   * :func:`.execute`
+   * :func:`.jump`
+   * :func:`.call`
+   * :func:`.set`
+   * :func:`.get`
+   * :func:`.terminate`
+   * :func:`.reload`
+   * :func:`.find_func`
+   * :func:`.find_label`
+   * :func:`.pause`
+   * :func:`.exec_line`
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.start
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.ready
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.add_lines
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.execute
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.jump
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.call
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.set
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.get
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.terminate
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.reload
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.find_func
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.find_label
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.pause
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.exec_line
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once).
+disable=R0201,C0103,W0403,W0611,W0614,W0401
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=no
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (RP0004).
+comment=no
+
+
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed. Python regular
+# expressions are accepted.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
+""" Test runner
+
+Preforms path mangling magic required to run test.
+"""
+import unittest
+import os, sys
+
+cwd = os.getcwd()
+# Append the inner package folder to the path so test can access internals
+#sys.path.append(os.path.join(cwd, 'foo'))
+
+# Gather tests
+from test import all_tests
+
+# Run tests
+unittest.TextTestRunner(verbosity=2).run(all_tests)
+from distutils.core import setup
+
+import ahk
+
+setup(name='pyahk',
+      version=ahk.__version__,
+      author='Ed Blake',
+      author_email='kitsu.eb@gmail.com',
+      url='https://bitbucket.org/kitsu/pyahk',
+      description='AutoHotKey.dll wrapped by ctypes.',
+      long_description=ahk.__doc__,
+      license="Modified BSD",
+      py_modules=['ahk'],
+      keywords=['Windows','Automation','AutoHoKey'],
+      classifiers=[
+          'Development Status :: 3 - Alpha',
+          'Environment :: Console',
+          'Intended Audience :: Developers',
+          'License :: OSI Approved :: BSD License',
+          'Operating System :: Microsoft :: Windows',
+          'Programming Language :: Python',
+          'Programming Language :: Python :: 2.7',
+          'Topic :: Software Development :: Testing',
+          'Topic :: System :: Automation',
+          'Topic :: Utilities',
+      ],
+)
+"""Bundle tests as a module."""
+import unittest
+
+import test_ahk
+
+# Gather all sub-tests into one suite
+all_tests = unittest.TestSuite([
+                                test_ahk.all_tests,
+                              ])
+"""Test AutoHotKey wrappers."""
+import os, time
+import unittest
+import ahk
+
+class Test_functions(unittest.TestCase):
+    """Test ahk misc. function wrappers."""
+
+    def setUp(self):
+        """Configure test environment."""
+        self.tempfilename = os.path.join(os.getcwd(), 
+                                         "{0}.ahk".format(int(time.time())))
+
+    def test_0_ready(self):
+        """Testing ahk.ready function."""
+        self.assertFalse(ahk.ready(nowait=True), 
+                         msg="Un-initialized library reports ready?")
+        self.assertFalse(ahk.ready(retries=5), 
+                         msg="Un-initialized library reports ready?")
+        # Next test depends on ahk.start
+        ahk.start()
+        self.assertTrue(ahk.ready(retries=15), 
+                         msg="Library Un-initialized after 15 tests?")
+
+    def test_1_start(self):
+        """Testing ahk.start function."""
+        if ahk.ready(nowait=True):
+            raise EnvironmentError("ahk script started before start test!")
+
+        # Test basic startup
+        ahk.start()
+        self.assertTrue(ahk.ready(retries=15), 
+                         msg="Library Un-initialized after 15 tests?")
+        ahk.terminate()
+
+        #NOTE no further test because passed arguments seem to be ignored.
+
+    def test_2_terminate(self):
+        """Testing terminating an ahk thread."""
+        if ahk.ready(nowait=True):
+            raise EnvironmentError("ahk script started before start test!")
+
+        # Confirm start, then confirm termination
+        ahk.start()
+        self.assertTrue(ahk.ready(retries=15), 
+                         msg="Library Un-initialized after 15 tests?")
+        ahk.terminate()
+        self.assertFalse(ahk.ready(nowait=True), 
+                         msg="Thread not terminated!")
+
+    def test_3_setget(self):
+        """Test setting/getting variable value."""
+        test_vals = [
+            1,
+            5,
+            50,
+            500,
+            'abc',
+            'a longer string',
+            'a string with\nspecial characters!'
+        ]
+
+        ahk.start()
+        ahk.ready()
+        # Test setting and then getting various variable values
+        for val in test_vals:
+            # Check setting is reported successful
+            self.assertTrue(ahk.set("test", val), 
+                            msg="Setting value reported as failed by ahk!")
+
+            # Check that setting was successful
+            value = ahk.get("test")
+            self.assertEqual(str(val), value,
+                             msg="Returned value {0} doesn't match {1}!".format(
+                                 value, val))
+
+        # Test getting a non-existent variable
+        self.assertEquals("", ahk.get("nonexistent"),
+                          msg="Got non-empty result from non-existent variable!")
+
+    def test_4_execute(self):
+        """Testing executing script strings."""
+        val = 0
+        arr = [5, 6, 3, 7, 2, 4, 1, 8, 9]
+        ahk.start()
+        ahk.ready()
+        # First set a test variable to a known value
+        self.assertTrue(ahk.set("test", val), 
+                        msg="Setting value reported as failed by ahk!")
+        # Execute a command that changes the value
+        self.assertTrue(ahk.execute("test := test+1"),
+                        msg="Execute reported failure!")
+        # Check the value has changed, and is correct
+        value = int(ahk.get("test"))
+        self.assertNotEqual(val, value, msg="Value unchanged after execution?")
+        self.assertEqual(value, 1,
+                         msg="Unexpected value {0} after execution!".format(value))
+        # Execute a more complicated set of commands and check the result
+        self.assertTrue(ahk.execute("arr={0}\nsort arr, N D,".format(
+                                    ",".join(str(i) for i in arr))),
+                        msg="Execute reported failure!")
+        value = ahk.get("arr")
+        arr = ",".join(str(i) for i in sorted(arr))
+        #print "result:\n\t{0}\n\t{1}".format(value, arr)
+        self.assertEqual(arr, value,
+                         msg="Unexpected result:\n\t{0} not equal to\n\t{1}".format(
+                             value, arr))
+        
+    def test_5_add_lines(self):
+        """Test adding code to running script."""
+        ahk.start()
+        ahk.ready()
+        # Add lines by string
+        self.assertEqual(ahk.get("test"), "", msg="Residual data found?")
+        ahk.add_lines("test = 5\n")
+        value = ahk.get("test")
+        self.assertEqual(value, "5",
+                         msg="Value={0}, script not evaluated?".format(value))
+
+        # Add lines by file path
+        with open(self.tempfilename, 'w') as tmp:
+            tmp.write("test := test+5")
+        addr = ahk.add_lines(filename=self.tempfilename)
+        #NOTE depends on ahk.exec_line
+        ahk.exec_line(addr, wait=True)
+        value = ahk.get("test")
+        self.assertEqual(value, "10",
+                         msg="Value={0}, script not evaluated?".format(value))
+
+    def test_6_jump(self):
+        """Testing jumping to a labeled code block."""
+        ahk.start()
+        ahk.ready()
+        # Add lines by string
+        self.assertEqual(ahk.get("test"), "", msg="Residual data found?")
+        ahk.add_lines("test = 0\nlbl:\ntest := test+5")
+        value = ahk.get("test")
+        self.assertEqual(value, "5",
+                         msg="Value={0}, script not evaluated?".format(value))
+
+        # Test jumping once
+        self.assertTrue(ahk.jump("lbl"), msg="Label not found in script!")
+        value = ahk.get("test")
+        self.assertEqual(value, "10",
+                         msg="Value={0}, script not evaluated?".format(value))
+
+        # Test repeated jump
+        for i in range(8):
+            self.assertTrue(ahk.jump("lbl"), msg="Label not found in script!")
+            expected = str(15+(i*5))
+            value = ahk.get("test")
+            self.assertEqual(value, expected,
+                             msg="Expected {0}, found {1}!".format(
+                                 expected, value))
+
+    @unittest.skip("Function calling broken.")
+    def test_7_call(self):
+        """Testing calling ahk functions."""
+        ahk.start()
+        ahk.ready()
+        # Add lines by string containing a function definition
+        ahk.add_lines("""
+                      Add(x, y) {
+                          return x + y   ; "Return" expects an expression.
+                      }
+                      """)
+        # Now call the new function and check the result
+        ahk.execute("result := Add(5,5)\n")
+        print ahk.get("result")
+        result = ahk.call("Add", 5, 5)
+        self.assertEqual(int(result), 10,
+                         msg="Unexpected result {0}!".format(result))
+
+    def tearDown(self):
+        """Clean test environment."""
+        # This fails if the terminate function fails.
+        ahk.terminate()
+        if os.path.exists(self.tempfilename):
+            os.remove(self.tempfilename)
+
+class Test_object(unittest.TestCase):
+    """Test ahk object wrappers."""
+
+    def setUp(self):
+        """Configure test environment."""
+        pass
+
+    def test_0_Function(self):
+        """Testing Function wrapper object."""
+        tests = (
+            (1, 1),
+            ('2', 2),
+            ('3', '4'),
+            (10, 11),
+            (100, 1000),
+        )
+        # Startup
+        ahk.start()
+        ahk.ready()
+        # Define our function
+        add = ahk.Function('add', int, '(x, y)', 'return x + y')
+        # Test with an assortment of values
+        for x, y in tests:
+            result = add(x, y)
+            expect = int(x) + int(y)
+            self.assertEqual(result, expect,
+                             msg="Unexpected result {0}, expected {1}!".format(
+                             result, expect))
+
+        with self.assertRaises(ValueError):
+            # Error during type conversion
+            add('abc', 'efg')
+        # Cleanup
+        ahk.terminate()
+
+    def test_1_ScriptFunction(self):
+        """Testing creating and using a Function through a Script object."""
+        tests = (
+            (1, 1),
+            ('2', 2),
+            ('3', '4'),
+            (10, 11),
+            (100, 1000),
+        )
+        script = ahk.Script()
+        # Define our good function
+        add = script.function('add', int, '(x, y)', 'return x + y')
+        self.assertIsInstance(add, ahk.Function,
+                              msg="Non-function {0} returned!".format(add))
+        # Test expected exceptions
+        with self.assertRaises(AttributeError):
+            script.function('_badname')
+            script.function('function')
+            script.function('add')
+        # Test with an assortment of values
+        for x, y in tests:
+            result = script.add(x, y)
+            expect = int(x) + int(y)
+            self.assertEqual(result, expect,
+                             msg="Unexpected result {0}, expected {1}!".format(
+                             result, expect))
+
+        with self.assertRaises(ValueError):
+            # Error during type conversion
+            script.add('abc', 'efg')
+
+    def test_2_ScriptVariable(self):
+        """Testing creating and using variables through a Script object."""
+        value = 5
+        script = ahk.Script()
+        # Define our good variable
+        script.variable('test', int, value)
+        # Test expected exceptions
+        with self.assertRaises(AttributeError):
+            script.function('_badname')
+            script.function('function')
+            script.function('test')
+        # Test getting variable value
+        self.assertEqual(script.test, value,
+             msg="Variable value {0} doesn't match expected {1}!".format(
+                 script.test, value))
+        # Test setting variable value
+        value = 10
+        script.test = 10
+        self.assertEqual(script.test, value,
+             msg="Variable value {0} doesn't match expected {1}!".format(
+                 script.test, value))
+        # Test outside modification
+        ahk.execute("test := test+5\n")
+        self.assertEqual(script.test, value+5,
+             msg="Variable value {0} doesn't match expected {1}!".format(
+                 script.test, value+5))
+
+    def test_3_winActive(self):
+        """Testing IfWinActive wrapper."""
+        script = ahk.Script()
+        # Test that non-existent window isn't found
+        name = "non-existent-{0}".format(time.time())
+        result = script.winActive(name)
+        self.assertEqual(result, None,
+                         msg="Found unexpected window with name \"{0}\"!".format(
+                             name))
+        # Test that an active window is findable
+        result = script.winActive("A") # "A" is the current active window
+        self.assertNotEqual(result, None,
+                            msg="Can't find an active window?")
+
+    def test_4_winExist(self):
+        """Testing IfWinExist wrapper."""
+        script = ahk.Script()
+        # Test that non-existent window isn't found
+        name = "non-existent-{0}".format(time.time())
+        result = script.winExist(name)
+        self.assertEqual(result, None,
+                         msg="Found unexpected window with name \"{0}\"!".format(
+                             name))
+        # Test that an active window is findable
+        result = script.winExist("A") # "A" is the current active window
+        self.assertNotEqual(result, None,
+                            msg="Can't find an active window?")
+
+    def tearDown(self):
+        """Clean test environment."""
+        pass
+
+# Assemble test suites
+function_suite = unittest.TestLoader().loadTestsFromTestCase(Test_functions)
+object_suite = unittest.TestLoader().loadTestsFromTestCase(Test_object)
+all_tests = unittest.TestSuite([
+                                function_suite, 
+                                object_suite, 
+                              ])
+if __name__ == "__main__":
+    # Run tests
+    unittest.TextTestRunner(verbosity=2).run(all_tests)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.