#!/usr/bin/env python3

###############################################################################
#
# Copyright (c) 2022-2025, Anders Andersen, UiT The Arctic University
# of Norway. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# - Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.
#
# - Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in
#   the documentation and/or other materials provided with the
#   distribution.
#
# - Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived
#   from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
###############################################################################


R"""The `webinteract` module for simple web interaction

This module implements a simplified web interaction class. It is
implemented using the [`splinter`](https://splinter.readthedocs.io)
module (and `splinter` is implemented using the
[`selenium`](https://selenium-python.readthedocs.io) module).

The easiest way to install this module is to use `pip`:

```bash
pip install webinteract
```

This `pip` command will install the module and a console script
`webinteract` to use the module as a program. If you execute
`webinteract` without a web-interaction-action script (`wia` script)
as an argument, it will open a browser window and present you a prompt
in the terminal where you do your interaction with the web. For more
information, try either the `-h` argument to the console script
`webinteract` or type `help` in the prompt.

The web interaction approach of this module is to use simple web
interaction scripts where each line is a web interaction action.  The
web intedraction class `WebInteraction` implements all the different
action types, and it is meant to be easily extendible. This is an
example of a small web interaction actions script (stored in a file
`"add-ts.wia"`):

```python
#!/usr/bin/env webinteract
setvals(url="https://a.web.page/", account="an@email.address")
visit(url)
fill(account, "id", "loginForm:id")
fill(pw, "id", "loginForm:password")
click("id", "loginForm:loginButton")
verify( \
  is_text_present("Login succeeded", wait_time=19), True, \
  "The text 'Login succeeded' is not present")
fill(date, "id", "registerForm:timeStamp")
click("id", "registerForm:addTS")
verify( \
  is_text_present("Register time stamp failed"), False, \
  "The text 'Register time stamp failed' is present")
```

The first line is special kind of comment that will be discussed
later. The second line gives vaules to two variables, `url` and
`account`. If we look closer at the script, we see a few other
varibles also used in the script (`pw` and `date`). They have to be
added to the name space of the script with the `update` method or as a
command line argument before the script is executed.

To perform the small web interaction script above we can do this in
Python:

```python
# Import modules used in the example
from datetime import datetime
import webinteract			# This module

# Create a time stamp (an example variable used it the script)
ts = datetime.now().strftime("%Y%m%d-%H%M%S")

# Open browser
web = webinteract.WebInteraction()

# Add `pw` and `date` to the name space of the script
web.update(pw = "a s3cret p4ssw0rd", date = ts)

# Perform web interaction actions (the script)
web(open("add-ts.wia"))
```

Another approach is to execute the script directly. The first line of
web interaction actions script (`wia` script) is a comment with a
magic string telling the system what program to use to interpret the
script. In this example, the installed console script
`webinteract`. For this to work, the `wia` script file has to be
executable (`chmod +x add-ts.wia`). The only other thing we have to
remember is to provide values for the two unasigned variables in the
script.  We provide this with the `--name-space` argument (in JSON
format):

```bash
./add-ts.wia --name-space '{"pw": "a s3cret p4ssw0rd", "date": "20250102-030405"}'
```

In real usage, you should of course never include a password in plain
text. The `webinteract` module supports storage of passwords in a
keychain/keyring using the Python module `keyring`.

For more information, check out the following blog post:

 - https://blog.pg12.org/web-page-interaction-in-python

To print the help message with all the command line arguments for the
`webinteract` console script use the command line argument `--help`:

```bash
webinteract --help
```

To print this documentation and all available web actions use the
command line argument `--doc [ACTION]` (where the optional argument is
used to print the documentation of a specific web action):

```bash
webinteract --doc
```

"""


#
# Import some modules needed (other modules imported below in source
# code if needed)
#

# Need these
import sys, json
from functools import wraps
from pathlib import Path

# For the help command in interactive mode
from inspect import signature

# Used for the wait action
from time import sleep

# For type hints
from typing import Any, Literal, TypedDict, TextIO
from collections.abc import Callable

# We use splinter to implement the web interaction
# https://splinter.readthedocs.io/
import splinter
from splinter.element_list import ElementList


#
# Help variables/functions
#

# Current version of module
version = "1.51"

# Mapping to some functions from the spliter Driver API (and Browser API)
# https://splinter.readthedocs.io/en/stable/api/driver-and-element-api.html

# An wia-action can take any arguments and return anything
type ActionFunc = Callable[..., Any]
type MakeActionFunc = Callable[WebInteraction, ActionFunc]

# A stored wia-action is a dictionary with the wia-action, its name
# and its arguments (both in the *args and *kw form)
StoredAction = TypedDict(
    "StoredAction", {
        "action": ActionFunc, "name": str, "args": list, "kw": dict})

# A function wrapping the actual action returning the a StoredAction
type ActionWrapper = Callable[..., StoredAction]

# The action groups (e.g., find_by_css, find_by_id, ...)
type ActionType = \
    Literal["find", "is_element_present", "is_element_not_present"]
_action_types: list[ActionType] = list(ActionType.__value__.__args__)

# Find element based on what?
type SelectorType = \
     Literal["css", "id", "name", "tag", "text", "value", "xpath"]
_selector_types: list[SelectorType] = list(SelectorType.__value__.__args__)

# Find links based on what?
type LinkType = \
    Literal["text", "partial_text", "href", "partial_href"]
_link_types: list[LinkType] = list(LinkType.__value__.__args__)

# The type of an API map to the Driver API (and Browser API) functions
EFuncMap = TypedDict("EFuncMap", {key: ActionFunc for key in _selector_types})
LFuncMap = TypedDict("LFuncMap", {key: ActionFunc for key in _link_types})
APIMap = TypedDict(
    "APIMap", {key: EFuncMap for key in _action_types} | {"link": LFuncMap})

def _mk_api_map(the_browser: splinter.Browser) -> APIMap:
    R"""Create an API map to the Driver API (and Browser API) functions

    In the map (a dictionary), the functions can be accessed using an
    `_action_types` key and an `_selector_types` or `_link_types` key.
    Some examples of the content of the API map:

    ```
    api_map["find"]["id"] -> the_browser.find_by_id
    api_map["link"]["text"] -> the_browser.links.find_by_text
    ```

    Arguments/return value:

    `the_browser`: An instance of the Browser class with the functions
    (methods) to link to in the API map.

    `return`: The API map (a dictionary)

    """
    api_map: APIMap = {}
    for atype in _action_types:
        api_map[atype] = {}
        for stype in _selector_types:
            api_map[atype][stype] = \
                getattr(the_browser, atype + "_by_" + stype)
    api_map["link"] = {}
    for ltype in _link_types:
         api_map["link"][ltype] = \
             getattr(the_browser.links, "find_by_" + ltype)
    return api_map

# Some help functions for stored wia-actions

def _is_action(action: StoredAction, name: str = None) -> bool:
    R"""Is this an action (with a given name)

    Returns `True` if `action` is a stored wia-action. A stored
    wia-action is a dictionary including the keys `"action"`,
    `"name"`, `"args"`, and `"kw"`.  If the `name` argument is
    provided it has to match the name of the action.

    Arguments/return value:

    `action`: Verify that this is actually a stored wia-action

    `name`: If given, the action should match the name

    `return`: Returns `True` if `action` is a stored wia-action (and
    if name is provided, it matches the name of the action)

    """

    if type(action) is dict:
        if all(k in action for k in ("action", "name", "args", "kw")):
            if name and action["name"] == name:
                return True
            elif not name:
                return True
    return False
        
def _do_inline_action(action: StoredAction) -> Any:
    R"""Execute the stored wia-action

    Perform the action stored in the dictionary with the store
    arguments.  `action["action"]` is the action (function) to call,
    and `action["args"]` and `action["kw"]` are the arguments.

    Arguments/return value:

    `action`: The stored wia-action (a dictionary including the actual
    function and its arguments)

    `return`: Whatever the action returns

    """
    return action["action"](*action["args"], **action["kw"])

def _prep_element(element: StoredAction | Any) -> Any:
    R"""If element is stored action, perform it

    Prepare the element: if the the element is an Action perform the
    action and return the result, otherwise return the element.

    Arguments/return value:

    `element`: The element to prepare

    `returns`: An element

    """
    return _do_inline_action(element) if _is_action(element) else element

def _element_action_it(
        element: ElementList, index: int | None,
        action_str: str, *args: tuple, doall: bool = False, **kw: dict):
    R"""Perform an action on a web element

    Perform an action on a web element (`"fill"`, `"check"`, `"click"`
    and so on).

    Arguments/return value:

    `element`: A list of elements (result for a find operation). Often
    a list with a single element.

    `index`: Used to choose the actual element from the list of
    elements. Usually we choose the first element (index is 0).

    `action_str`: The name of the action performed (e.g, `"fill"`,
    `"check"`, `"click"`).

    `*args`: Positional arguments to the action performed.

    `doall`: If index is `None` and this is `True`, the action is
    performed on all matching web elements.

    `**kw`: Named arguments to the action performed.

    """
    failed = True	# Pesimistic
    if type(element) is ElementList:
        if index != None:
            elist = [element[index]]
        else:
            elist = element
        for e in elist:
            if hasattr(e, action_str):
                try:
                    action = getattr(e, action_str)
                    action(*args, **kw)
                    failed = False
                    if not doall:
                        break
                except:
                    continue
    if failed:
        raise WebInteractionError(
            f"Web-element action failed: element[{index}].{action_str} " + \
            f"not found")

def _element_action(
        element: StoredAction | ElementList, index: int | None,
        action_str: str, *args: tuple, doall: bool = False, **kw: dict):
    R"""Find and perform an action on a web element

    Find the web-element using the provided `find_action` (a stored
    action) and then perform the action.

    Arguments/return value:

    `element`: A web element or a stored action used to find the web element.

    `index`: Used to choose the actual element from the list of
    elements. Usually we choose the first element (`index` is 0).

    `action_str`: The name of the action performed (e.g, `"fill"`,
    `"check"`, `"click"`).

    `*args`: Positional arguments to the action performed.

    `doall`: If index is `None` and this is `True`, the action is
    performed on all matching web elements.

    `**kw`: Named arguments to the action performed.

    """
    if _is_action(element):
        element = _do_inline_action(element)
    _element_action_it(element, index, action_str, *args, doall=doall, **kw)


#
# The web interact actions (wia) language implementation
#


def webaction(func: MakeActionFunc) -> ActionWrapper:
    R"""Web action decorator

    A decorator that wraps a web action. The wrapper stores the
    web-action in a dictionary structure and returns it. This stored
    web-action (dictionary) is later used to actually perform the call
    in a limited name space (using the `WebInteraction` class method
    `_do_action`).

    Arguments/return value:

    `func`: a function returning a web action. The name of the action
    is the name of the fuction except the four first characters
    (remove the `"_mk_"` part).

    `return`: A wrapper for the web action that returns a stored
    action.

    """

    # Grab the name (removing the "_mk_" part)
    name: str = func.__name__[4:]

    # The wrapper function
    @wraps(func) # Update the wrapper function to look like the wrapped func
    def wrapper(self, *args: tuple, **kw: dict) -> StoredAction:

        # Grab the action (returned by the `_mk_...` method)
        action: ActionFunc = func(self)

        # Store the action in a dictionary and return it
        return {
            "action": action,
            "name": name,
            "args": args,
            "kw": kw
        }

    # Replace the the make action method with the wrapper
    return wrapper


class WebPrompt:

    def __init__(self, docs, each_line, args_dict):

        self._docs = docs
        self._each_line = each_line

        # Colors: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE
        # Supported, but not part of the Ansi standard: LIGHTBLACK_EX,
        # LIGHTRED_EX, LIGHTGREEN_EX, LIGHTYELLOW_EX, LIGHTBLUE_EX,
        # LIGHTMAGENTA_EX, LIGHTCYAN_EX, LIGHTWHITE_EX
        from colorama import Fore
        if args_dict:
            prompt = "\033[1m" + args_dict["prompt"] + "\033[0m"
            prompt_color = eval("Fore." + args_dict["prompt_color"])
            output_color = eval("Fore." + args_dict["prompt_output_color"])
            alert_color = eval("Fore." + args_dict["prompt_alert_color"])
        else:
            prompt = "\033[1m" + "wia> " + "\033[0m"
            prompt_color = Fore.CYAN
            output_color = Fore.GREEN
            alert_color = Fore.RED
        self.message = prompt_color + prompt + Fore.RESET
        self.output = output_color + "%(msg)s" + Fore.RESET
        self.alertit = alert_color + "%(msg)s" + Fore.RESET
        
        # Use `prompt_toolkit`
        from prompt_toolkit import PromptSession, print_formatted_text, ANSI
        from prompt_toolkit.history import FileHistory
        self.printit = lambda m, *a, **k: print_formatted_text(ANSI(m), *a, **k)
        self.session = PromptSession(
            message = ANSI(self.message),
            history = FileHistory(Path.home() / ".wia_history"))
        
        # Map to the shell specific commands
        self.shell_cmd = {
            #"history": self.history, "hdrop": self.hdrop,
            #"hclear": self.hclear,
            "help": self.help, "doc": self.doc, "exit": self.exit}

        # Add prompt command help (documentation)
        self.prompt_cmd_help = ""
        for cmd in self.shell_cmd.keys():
            self.prompt_cmd_help += "\n" + cmd + ": " + \
                self.shell_cmd[cmd].__doc__

        # Add action specific help functions
        for adoc in self._docs:
            self.shell_cmd["help " + adoc] = self.help

    # Some shell specific commands:

    # Help
    def help(self, line):
        R"""Print help text"""
        if line[5:] in self._docs:
            self.out(self._docs[line[5:]])
        else:
            actions = _list_actions(self._docs)
            self.out(
                "Available web actions:\n" + \
                "======================\n" + \
                actions + "\n" + \
                "-------------------------------\n" + \
                "Use 'help <action>' for details\n" + \
                "-------------------------------\n" + \
                "The prompt commands:" + \
                self.prompt_cmd_help)

    # The module doc string
    def doc(self, line):
        R"""Print module documentation"""
        self.out(__doc__.strip() + "\n")

    # Exit prompt
    def exit(self, line):
        R"""Exit interactive prompt"""
        return "exit"

    # The interactive prompt and processing

    def _msg(self, msg, what, *args, **kw):
        self.printit(what % {"msg": msg}, *args, **kw)

    def out(self, msg):
        self._msg(msg, self.output)

    def alert(self, msg):
        self._msg(msg, self.alertit, flush=True)
    
    # Present prompt and get input
    def get_input(self):
        return self.session.prompt()
        
    # Process input
    def process_input(self, line):
        stripped_line: str = line.strip()
        if stripped_line in self.shell_cmd:
            self.shell_cmd[stripped_line](stripped_line)
        elif stripped_line[:5] == "help ":
            self.alert(f"Help: unknown action '{stripped_line[5:]}'\n")
        elif stripped_line == "":
            pass
        else:
            try:
                result = self._each_line(line)
                if result != None:
                    self.out(repr(result))
                    return result
            except Exception as err:
                self.alert(
                    f"Web page interaction failed: '{line}'\n" + \
                    f"Error: {err}\n")


class WebInteractionError(Exception):
    R"""Web interaction error

    An error raised when a web interaction action (web action) fails.

    """
    pass


class WebInteraction:
    R"""The web interaction class for web sessions

    The web interaction class `WebInteraction` creates web interaction
    objects that can interpret a series of web interaction actions
    (web-actions).  When an instance of the class is created, a web
    session, with a companion web browser where the web actions are
    performed, is also created. The class implements all the web
    actions and, in addition, the following methods for preparing,
    performing, and terminating the web interaction session:

     - `update`: Update the namespace for the web actions

     - `execute`: Perform the web actions from a `wia` script or
       interactivly (presenting a web interaction prompt).  This
       method is also called when an instance of the `WebInteraction`
       class is called as a function, and it is the main function of
       the `WebInteraction` class.

     - `quit`: Exit the web interaction session (and the web browser
       created when `WebInteraction` was instanciated).

    """

    def __init__(self, *args: tuple, **kw: dict):
        R"""Instanciate a web interaction object

        When a web interaction object is created, a web session is
        started and all the preparations to perform web actions are
        done. This includes creating the initial namespace for the web
        actions and creating the web browser where the actions are
        performed.
        
        The arguments are equal to the arguments used to instanciate a
        `Browser` object from the `splinter` module.  For details, see the
        documentation at

         - https://splinter.readthedocs.io/

        If the default behavior of the `Browser` class from `splinter`
        is OK, no arguments are necessary.

        Arguments:

        `*args`: positional arguments to the the `Browser` constructor

        `**kw`: named arguments to the the `Browser` constructor

        """

        # The browser used to interact with the web pages
        self._browser: splinter.Browser = splinter.Browser(*args, **kw)
        self._api_map: APIMap = _mk_api_map(self._browser)

        # The available functions in the name space of the web actions:
        # Find all methods starting with _mk_ and add them to namespace
        self._ns: dict[str, Any] = {}
        for k in dir(self):
            if k[:4] == "_mk_":	
                self._ns[k[4:]] = getattr(self, k)
        
        # The name space can be populated with other names, but the
        # ones initialized here are read-only (safe keys) and should
        # never be updated
        self._safekeys: list = list(self._ns.keys())

        # Module and action documentation
        self._docs = _make_docs()

        # Current action (code string) and line number (in a wia-script)
        self._current_action: str | None = None
        self._lineno: int = 0

        # To handle multiline commands (lines ending with '\')
        self._multi: str = ""

    def _do_action(self, action_code: str):
        R"""Perform a web interaction action

        Perform the saved web interaction action, and if specified,
        check the reurn value of the action.

        Arguments/return value:

        `action`: The action (a line in the web interaction action script)

        """

        # Save current action (string)
        self._current_action = action_code

        # Create stored action
        try:
            action: StoredAction = eval(
                action_code, {"__builtins__": None}, self._ns)
        except Exception as err:
            msg = f"Create action failed: {action_code} \n"
            msg += f"{str(err)}"
            raise WebInteractionError(msg)
            
        # Perform the action and save the return value (the result)
        try:
            result = action["action"](*action["args"], **action["kw"])
        except Exception as err:
            msg = f"Perform action failed: {action_code} \n"
            msg += f"{str(err)}"
            raise WebInteractionError(msg)

        # Drop current action
        self._current_action = None

        # Return result (might be None and/or ignored)
        return result

    def _each_line(self, line):
        
        # Count current line and remove any spaces before and after
        self._lineno += 1
        stripped_line: str = line.strip()

        # Ignore comments line
        if stripped_line and stripped_line[0] == "#":
            return

        # A multi-line action is merged to a single line
        if stripped_line and stripped_line[-1] == "\\":
            self._multi += stripped_line[:-1]
            return
        else:
            stripped_line = self._multi + stripped_line
            self._multi = ""

        # Perform the action if it is not empty
        if stripped_line:
            return self._do_action(stripped_line)

    def __call__(self, *args, **kw):
        self.execute(*args, **kw)

    def execute(self, file: TextIO | None = None,
                args_dict: dict | None = None):
        R"""Read and and execute the web interaction action script

        This perform the actual execution of the web interaction
        action script using the web interaction action language
        defined by the functions in the web interaction action name
        space.

        This method is called when an instance of the `WebInteraction`
        class is called like a function.

        Arguments:

        `file`: The file object with the web interaction action script
        (any object implementing Python text i/o will do:
        https://docs.python.org/3/library/io.html). If this is `None`,
        read web interaction actions from a prompt.

        """

        # Read web interaction actions from the wia script file
        if file or not sys.stdout.isatty():

            # Read from stdin
            if not file:
                file = sys.stdin

            # No prompt
            prompt = None
        
            # Read and execute each line of the script
            for line in file:
                result = self._each_line(line)
                if result != None:
                    print(result, file=args_dict["output_file"])
                
        # If no file, read web interaction actions from a prompt   
        else:

            # Prepare web prompt
            prompt = WebPrompt(self._docs, self._each_line, args_dict)

            # Get input until exit
            while (line := prompt.get_input()) != 'exit':
                result = prompt.process_input(line)
                if 'exit' in line and result == 'exit':
                    break
                elif result != None:
                    print(result, file=args_dict["output_file"])

        # Hmm, a left over action (non terminated multi line)
        if self._multi and self._multi[0] != "#":
            self._do_action(self._multi)
            
    def update(self, *args: tuple[dict[str, Any]], **kw: dict[str, Any]):
        R"""Update the namespace

        Update the web interaction action namespace with variables
        that can be used in the scripts (typically values used as
        arguments to the fuctions that can not be specified directly
        in the scripts). If `web` is a `WebInteraction` instance,
        these two `update` calls perform the same update:

        ```python
        web.update({"pw": "a s3cret p4ssw0rd", "date": ts})
        web.update(pw = "a s3cret p4ssw0rd", date = ts)
        ```

        Arguments:

        `*args`: positional arguments, name space (dictionary)
        mapping names to values.

        `**kw`: named arguments, mapping names to values

        """

        # Get the update namespace from the arguments
        ns = {}
        if args:
            for n in args:
                ns.update(n)
        if kw:
            ns.update(kw)

        # Is the keys in the updated name space valid?
        for key in ns.keys():
            if key in self._safekeys:
                raise PermissionError(
                    f"Can not update read-only '{key}' in name space")

        # Update the name space
        self._ns.update(ns)

    def quit(self):
        R"""Quit the browser

        Quit the browser and terminate the web interaction session.

        """
        self._browser.quit()

    def __setitem__(self, key: str, val: Any):
        R"""Add variable to name space

        Add a variable to the name space of the web interaction actions.

        Arguments/return value:

        `key`: the name of the variable

        `val`: the value of the variable
        
        """

        # Only change non safe (non read-only) variables
        if not key in self._safekeys:

            # Add or update variable with name `key` and value `val`
            self._ns[key] = val
            
        else:

            # Raise error of we try to update safe variable
            raise PermissionError(
                f"Can not update read-only '{key}' in name space")

    def __getitem__(self, key: str) -> Any:
        R"""Get variable from name space

        Return the value of a variable in the name space of the web
        interaction actions.

        Arguments/return value:

        `key`: the name of the variable to return the value of

        """

        # Safe keys are not returned (they are internal)
        if not key in self._safekeys:
            return self._ns[key]
        else:
            raise KeyError(f"{key}")

    def _do_find(
            self,
            stype: SelectorType, sval: str,
            element: ElementList = None, index: int = 0) -> ElementList:
        R"""Do different find actions

        Returns a list of elements matching the selector `stype` with
        the value `sval` (often containing a single element). If the
        `element` argument is given, find web elments conatined in the
        given element.

        Arguments/return value:

        `stype`: An SelectorType, meaning one of the following:
        `"css"`, `"id"`, `"name"`, `"tag"`, `"text"`, `"value"`, or
        `"xpath"`.

        `sval`: The value to match (e.g., the id of an element).

        `element`: A list of the web elements (default None -> find global).

        `index`: Choose from the list of elements (default 0).

        `return`: A list of matching elments (often, just a list of one).

        """
        if element:
            find_by_ = getattr(element[index], "find_by_" + stype)
            return find_by_(sval)
        elif stype in self._api_map["find"] and sval:
            return self._api_map["find"][stype](sval)
        else:
            raise WebInteractionError(
                f"find: Unknown type or missing value ({stype}: {sval})")

    def current_action_info(self) -> str:
        if self._current_action:
            return f'{self._lineno}: "{self._current_action}"'
        else:
            return ""

    # All the remaining method names starts with "_mk_" and they are
    # the predefined web interaction actions. Some functions are
    # implemented inside the method and some are mapping to the
    # coresponding `splinter` browser object. The name is created from
    # the method name (removing the "_mk_" part). The work is done in
    # the `webaction` decorater. The method should return a fuction
    # implementing the web interaction action.  Add new methods below
    # with the `webaction` decorator to extend the web interaction
    # action language (the wia language).

    @webaction
    def _mk_wait(self) -> ActionFunc:
        R"""Action `wait` waits for the given seconds in a script

        Used if you need to wait for web elements to be loaded or when
        debugging scripts. Some other actions, like `is_present`, can
        also wait for a while if the expected element is not present
        yet (they have an optional argument `wait_time`).

        Arguments:

        `wait_time`: The amount of time to wait in seconds (default 1).

        """

        # The implementation of the action
        def wait(wait_time: int = 1):
            sleep(wait_time)
        
        # Return the action
        return wait
    
    @webaction
    def _mk_setvals(self) -> ActionFunc:
        R"""Action `setvals` sets values used later in the script

        The `setvals` action can be used to give a value to one or more
        variables used later in the script.  This example sets the
        values of the two variables `url` and `email`:

        ```python
        setvals(url = "https://a.web.page/", email = "an@email.address")
        ```

        The variables `url` and `email` can then be used in other
        actions later in the script. `setvals` updates the name space
        of the script with the varibales with the given value.

        It is also possible to use web actions that returns a value
        with `setvals`. In this example we set the value of the
        variable `tag` to the value of an element with an id `"tag"`:

        ```python
        setvals(tag = get_value("id", "tag"))
        ```

        Arguments/return:

        `**kw`: Named arguments that set values in the namespace.

        """

        # The implementation of the action
        def setvals(**kw):

            # If the value is an action call, perform the action
            for k in kw:
                va = kw[k]
                kw[k] = _prep_element(va)
            
            # Update the namespace
            self.update(kw)

        # Return the action
        return setvals

    @webaction
    def _mk_verify(self) -> ActionFunc:
        R"""Action `verify` checks that two values match

        The created action checks that the two given value arguments
        match.  If they don't match and the `assert_it` argument is
        `True` (the default), the `WebInteractionError` is raised (the
        wia-script terminates). If they don't match and the
        `assert_it` argument is `False`, the action returns
        `False`. Each value argument is either a value or an
        action. If it is an action, the action is performed and the
        result of the action is the value compared with the other
        argument. Three examples:

        ```python
        verify(url, "https://a.web.page/")
        verify(is_present("id", "afilter"), True, "No filter")
        verify("web", get("id", "searchtxt"), "Action fill failed")
        ```

        The first example verifies that `url` has the value
        `"https://a.web.page/"`. The second example verifies that a web
        element with id `"afilter"` is present (`is_element_present`
        returns `True`). The third example verifies that a web element
        with id `"searchtxt"` has the value (or text) `"web"` (`get`
        returns `"web"`).

        The third optional argument is the error message given if the
        verification fails and the `WebInteractionError` is
        raised.

        Arguments/return value:

        `val1`: Value one.

        `val2`: Value two.

        `errmsg`: The error message given if the verification fails
        and the `WebInteractionError` is raised.

        `assert_it`: Should the action raise the `WebInteractionError`
        if the values don't match. The default is `True`.

        `return`: `True` if the two values match, `False`
        otherwise. If the `assert_it` argument is `True`, the
        WebInteractionError exception is raised if the two values do
        not match, and if they match, nothing is returned (and nothing
        happens).

        """

        # The implementation of the action
        def verify(
                val1: Any, val2: Any,
                errmsg: str = "No match",
                assert_it: bool = True) -> bool | None:
            v1 = _prep_element(val1)
            v2 = _prep_element(val2)
            if v1 != v2:
                if assert_it:
                    raise WebInteractionError(
                        f'Action "verify": failed: "{errmsg}" ({v1} != {v2})\n')
                else:
                    return False
            if not assert_it:
                return True

        # Return the action
        return verify
    
    @webaction
    def _mk_visit(self) -> ActionFunc:
        R"""Action `visit` is used to open a web page (URL)

        This action opens a web page (URL). The actions that follow
        will interact with this web page:

        ```python
        visit("https://a.web.page/")
        ```

        Actions following this action operates on this web page. The
        arguments to this action is the same as the `visit` method
        from the `Browser` class in the `splinter` module
        (https://splinter.readthedocs.io/en/latest/browser.html). To
        be more presise, the returned method is the `visit` method
        from the `Browser` class in the `splinter` module (and for moe
        detailed documentation, please use the `splinter` module
        documentation).

        Arguments/return value:

        `url`: The URL to be visited.

        """

        # The implementation of the action
        def visit(url: str):
            self._browser.visit(url)
        
        # Return the action
        return visit
    
    @webaction
    def _mk_find(self) -> ActionFunc:
        R"""Action `find` finds web elements on a web page

        This action finds web elements based on a selector type and
        the value of such a selector. This example returns a list of
        web elements with the id `"filter"` (often a list with a single
        element):

        ```python
        find("id", "filter")
        ```

        Another example using an XPath selector to find all `a`
        (anchor) elements with an attribute `title` that has the value
        `"log out"` (often a list with a single element):

        ```python
        find("xpath", "//a[@title='log out']")
        ```

        Arguments/return value:

        `stype`: The selector type (either `"css"`, `"id"`, `"name"`,
        `"tag"`, `"text"`, `"value"`, or `"xpath"`).

        `sval`: The value of the selector type.

        `return`: A list of the web elements matching the selector
        `stype` with the value `sval`.

        """

        # The implementation of the action
        def find(stype: SelectorType, sval: str) -> ElementList:
            return self._do_find(stype, sval)

        # Return the action
        return find
    
    @webaction
    def _mk_find_in_element(self) -> ActionFunc:
        R"""Action `find_in_element` finds web elements inside the web element

        This action finds web elements based on a selector type and
        the value of such a selector inside the given web
        element. This example returns a list of web elements with the
        name `"filter"` from inside a web element with the id
        `"form"`:

        ```python
        find_in_element("name", "filter", find("id", "form"), 0)
        ```

        Arguments/return value:
        
        `stype`: The selector type (either `"css"`, `"id"`, `"name"`,
        `"tag"`, `"text"`, `"value"`, or `"xpath"`).

        `sval`: The value of the selector type.

        `element`: Find the web element inside one of these elements.

        `index`: Choose from the list of elements (default 0).        

        `return`: A list of the web elements matching the selector
        `stype` with the value `sval`.

        """

        # The implementation of the action
        def find_in_element(
                stype: SelectorType, sval: str,
                element: ElementList, index: int = 0) -> ElementList:
            element = _prep_element(element)
            return self._do_find(stype, sval, element, index)

        # Return the action
        return find_in_element
    
    @webaction
    def _mk_element_get(self) -> ActionFunc:
        R"""Action `element_get` gets the value or text of the web element

        Get the value or text of the given web element.

        Arguments/return value:

        `element`: A list of the web elements.

        `index`: Choose from the list of matching elements (default 0).

        `return`: The value or text of a web element.

        """

        # The implementation of the action
        def element_get(element: ElementList, index: int = 0) -> str:
            element = _prep_element(element)[index]
            return element.value if element.value else element.text

        # Return the action
        return element_get
    
    @webaction
    def _mk_get(self) -> ActionFunc:
        R"""Action `get` gets the value or text of a web element

        Get the value or text of a web element matching the selector
        `stype` with the value `sval`. An example where we get the
        value or text of an element with the id `"about"`:

        ```python
        get("id", "about")
        ```

        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default 0).

        `return`: The value or text of a web element matching the
        selector `stype` with the value `sval`.

        """

        # The implementation of the action
        def get(stype: SelectorType, sval: str, index: int = 0) -> str:
            element = self._do_find(stype, sval)[index]
            return element.value if element.value else element.text

        # Return the action
        return get
    
    @webaction
    def _mk_element_get_value(self) -> ActionFunc:
        R"""Action `element_get_value` gets the value of the web element

        Get the value of the web element.
        
        Arguments/return value:

        `element`: A list of the web elements.

        `index`: Choose from the list of elements (default 0).
        
        `return`: The value of the web element.

        """

        # The implementation of the action
        def element_get_value(element: ElementList, index: int = 0) -> str:
            return _prep_element(element)[index].value

        # Return the action
        return element_get_value
    
    @webaction
    def _mk_get_value(self) -> ActionFunc:
        R"""Create action `get_value` gets the value of a web element

        Get the value of a web element matching the selector stype
        with the value sval. An example where we get the value of an
        element with the name `"aggregate"`:

        ```python
        get_value("name", "aggregate")
        ```
        
        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default 0).
        
        `return`: The value of a web elements matching the selector
        `stype` with the value `sval`.

        """

        # The implementation of the action
        def get_value(stype: SelectorType, sval: str, index: int = 0) -> str:
            element = self._do_find(stype, sval)[index]
            return element.value 

        # Return the action
        return get_value
    
    @webaction
    def _mk_element_is_checked(self) -> ActionFunc:
        R"""Action `element_is_checked` checks if the web element is checked

        Check if the web element (checkbox) is checked.
        
        Arguments/return value:

        `element`: A list of the web elements.

        `index`: Choose from the list of elements (default 0).
        
        `return`: True if the web element (checkbox) is checked.

        """

        # The implementation of the action
        def element_is_checked(element: ElementList, index: int = 0) -> bool:
            return _prep_element(element)[index].checked

        # Return the action
        return element_is_checked
    
    @webaction
    def _mk_is_checked(self) -> ActionFunc:
        R"""Create action `is_checked` checks if a web element is checked

        Returns true if a web element matching the selector stype with
        the value sval is checked. An example where we check
        an element with the name `"checkbox1"`:

        ```python
        is_checked("name", "checkbox1")
        ```
        
        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default 0).
        
        `return`: True if a web element (checkbox) matching the
        selector `stype` with the value `sval` is checked.

        """

        # The implementation of the action
        def is_checked(stype: SelectorType, sval: str, index: int = 0) -> str:
            element = self._do_find(stype, sval)[index]
            return element.checked 

        # Return the action
        return is_checked
    
    @webaction
    def _mk_element_get_text(self) -> ActionFunc:
        R"""Action `element_get_text` gets the text of the web element
        
        Returns true if te web element is checked.

        Arguments/return value:

        `element`:  A list of the web elements.

        `index`: Choose from the list of elements (default 0).
        
        `return`: The text of the web element.

        """

        # The implementation of the action
        def element_get_text(element: ElementList, index: int = 0) -> str:
            return _prep_element(element)[index].text

        # Return the action
        return element_get_text
    
    @webaction
    def _mk_get_text(self) -> ActionFunc:
        R"""Action `get_text` gets the text of a web element

        Get the text of a web element matching the selector stype with
        the value sval. An example where we get the text of the third
        element (at index 2) with the tag `"h2"`:

        ```python
        get_text("tag", "h2", 2)
        ```

        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default 0).
        
        `return`: The text of a web elements matching the selector
        `stype` with the value `sval`.

        """

        # The implementation of the action
        def get_text(stype: SelectorType, sval: str, index: int = 0) -> str:
            element = self._do_find(stype, sval)[index]
            return element.text

        # Return the action
        return get_text
    
    @webaction
    def _mk_element_check(self) -> ActionFunc:
        R"""Action `element_check` checks the web element (checkbox)

        Check the checkbox web element.
        
        Arguments/return value:

        `element`:  A list of the web elements.

        `index`: Choose from the list of elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def element_check(element: ElementList, index: int | None = None,
                          doall: bool = False):
            _element_action(element, index, "check", doall=doall)

        # Return the action
        return element_check

    @webaction
    def _mk_check(self) -> ActionFunc:
        R"""Action `check` checks a web element (checkbox)

        Check a checkbox web element matching the selector `stype`
        with the value `sval`. This example checks the fourth checkbox
        on the web page (with index 3):

        ```python
        check("xpath", "//input[@type='checkbox']", 3)
        ```

        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def check(stype: SelectorType, sval: str, index: int | None = None,
                  doall: bool = False):
            element = self._do_find(stype, sval)
            _element_action_it(element, index, "check", doall=doall)

        # Return the action
        return check
    
    @webaction
    def _mk_element_uncheck(self) -> ActionFunc:
        R"""Action `element_uncheck` unchecks the web element (checkbox)

        Uncheck the checkbox web element.

        Arguments/return value:

        `element`:  A list of the web elements.

        `index`: Choose from the list of elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def element_uncheck(element: ElementList, index: int | None = None,
                            doall: bool = False):
            _element_action(element, index, "uncheck", doall=doall)

        # Return the action
        return element_uncheck
    
    @webaction
    def _mk_uncheck(self) -> ActionFunc:
        R"""Action `uncheck` unchecks a web element (checkbox)

        Uncheck a checkbox web element matching the selector `stype`
        with the value `sval`. This example unchecks a checkbox with
        id `"include-comments"`:

        ```python
        uncheck("id", "include-comments")
        ```

        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def uncheck(stype: SelectorType, sval: str, index: int | None = None,
                    doall: bool = False):
            element = self._do_find(stype, sval)
            _element_action_it(element, index, "uncheck", doall=doall)

        # Return the action
        return uncheck
    
    @webaction
    def _mk_element_clear(self) -> ActionFunc:
        R"""Action `element_clear` clears the web element

        Reset the field value of the web element.

        Arguments/return value:

        `element`:  A list of the web elements.

        `index`: Choose from the list of elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def element_clear(element: ElementList, index: int | None = None,
                          doall: bool = False):
            _element_action(element, index, "clear", doall=doall)

        # Return the action
        return element_clear
    
    @webaction
    def _mk_clear(self) -> ActionFunc:
        R"""Action `clear` clears a web element

        Reset the field value of a web element matching the selector
        `stype` with the value `sval`. This example clears a field
        with the name `"search"`:

        ```python
        clear("name", "search")
        ```

        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def clear(stype: SelectorType, sval: str, index: int | None = None,
                  doall: bool = False):
            element = self._do_find(stype, sval)
            _element_action_it(element, index, "clear", doall=doall)

        # Return the action
        return clear
   
    @webaction
    def _mk_element_click(self) -> ActionFunc:
        R"""Action `element_click` clicks the web element (button)

        Click on the web element.
        
        Arguments/return value:

        `element`: A list of the web elements.

        `index`: Choose from the list of elements (default `None`).

        """

        # The implementation of the action
        def element_click(element: ElementList, index: int | None = None):
            _element_action(element, index, "click")

        # Return the action
        return element_click
    
    @webaction
    def _mk_click(self) -> ActionFunc:
        R"""Action `click` clicks a web element (button)

        Click on a web element matching the selector `stype` with the
        value `sval`. This example clicks on a web element with the
        text `"OK"` (typically a button):

        ```python
        click("text", "OK")
        ```

        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default `None`).

        """

        # The implementation of the action
        def click(stype: SelectorType, sval: str, index: int | None = None):
            element = self._do_find(stype, sval)
            _element_action_it(element, index, "click")

        # Return the action
        return click    

    @webaction
    def _mk_element_select(self) -> ActionFunc:
        R"""Action `element_select` selects the value in the web element

        Select the given value `val` in the select web the element.

        Arguments/return value:

        `val`: The value to fill in.

        `element`: A list of the web elements.

        `index`: Choose from the list of elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def element_select(val: str, element: ElementList,
                           index: int | None = None, doall: bool = False):
            _element_action(element, index, "select", val, doall=doall)

        # Return the action
        return element_select
    
    @webaction
    def _mk_select(self) -> ActionFunc:
        R"""Action `select` selects the value in a web element

        Select the given value `val` in a select web element matching
        the selector `stype` with the value `sval`. In this example,
        `"year"` is selected in the web element with the name
        `"type"`:

        ```python
        select("year", "name", "type")
        ```

        Arguments/return value:

        `val`: The value to fill in.

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def select(val: str, stype: SelectorType, sval: str,
                   index: int | None= None, doall: bool = False):
            element = self._do_find(stype, sval)
            _element_action_it(element, index, "select", val, doall=doall)

        # Return the action
        return select
    
    @webaction
    def _mk_element_fill(self) -> ActionFunc:
        R"""Action `element_fill` fills the value in the element

        Fill in the value `val` in the web element.
        
        Arguments/return value:

        `val`: The value to fill-in.

        `element`: A list of the web elements.

        `index`: Choose from the list of elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def element_fill(val: str, element: ElementList,
                         index: int | None = None, doall: bool = False):
            _element_action(element, index, "fill", val, doall=doall)

        # Return the action
        return element_fill
    
    @webaction
    def _mk_fill(self) -> ActionFunc:
        R"""Action `fill` fills the value in a web element

        Fill in the value `val` in a web element matching the selector
        `stype` with the value `sval`. In this example, a web element with
        the name `"search"` is filled with the text `"Python"`:

        ```python
        fill("Python", "name", "search")
        ```
        
        Arguments/return value:

        `val`: The value to fill-in.

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default `None`).

        `doall`: If index is `None` and this is `True`, the action is
        performed on all matching web elements.

        """

        # The implementation of the action
        def fill(val: str, stype: SelectorType, sval: str,
                 index: int | None = None, doall: bool = False):
            element = self._do_find(stype, sval)
            _element_action_it(element, index, "fill", val, doall=doall)

        # Return the action
        return fill

    @webaction
    def _mk_element_scroll_to(self) -> ActionFunc:
        R"""Action `element_scroll_to` scrolls to the web element

        Scroll to the web element.
        
        Arguments/return value:

        `element`: A list of the web elements.

        `index`: Choose from the list of elements (default `None`).

        """

        # The implementation of the action
        def element_scroll_to(element: ElementList, index: int | None = None):
            _element_action(element, index, "scroll_to")

        # Return the action
        return element_scroll_to
    
    @webaction
    def _mk_scroll_to(self) -> ActionFunc:
        R"""Action `scroll_to` scrolls to a web element

        Scroll to a web element matching the selector `stype` with the
        value `sval`. In this example, the view is scrolled to a `div`
        element with a `class` attribute having the value
        `"signature"`:

        ```python
        scroll_to("xpath", "//div[@class='signature']")
        ```

        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default `None`).

        """

        # The implementation of the action
        def scroll_to(stype: SelectorType, sval: str, index: int | None = None):
            element = self._do_find(stype, sval)
            _element_action_it(element, index, "scroll_to")

        # Return the action
        return scroll_to

    @webaction
    def _mk_find_link(self) -> ActionFunc:
        R"""Action `find_link` finds link elements

        The action `find_link` returns link web elements based on a
        link selector type and the value of such a link selector. This
        example returns a list of link elements with `href` attribute
        values containing `"filter"`:

        ```python
        find_link("partial_href", "filter")
        ```

        Arguments/return value:

        `ltype`: The link selector type.

        `lval`: The value of the link selector type.

        `return`: A list of matching link elements.

        """        

        # The implementation of the action
        def find_link(ltype: LinkType, lval: str) -> ElementList:
            return self._api_map["link"][ltype](lval)

        # Return the action
        return find_link
    
    @webaction
    def _mk_element_click_link(self) -> ActionFunc:
        R"""Action `element_click_link` clicks the link

        Click on the link element.

        Arguments/return value:

        `element`: A list of the web elements.

        `index`: Choose from the list of elements (default 0).

        """

        # The implementation of the action
        def element_click_link(element: ElementList, index: int = 0):
            element = _prep_element(element)
            element[index].click()

        # Return the action
        return element_click_link
    
    @webaction
    def _mk_click_link(self) -> ActionFunc:
        R"""Action `click_link` clicks a link

        Click on a link element matching the selector `ltype` with the
        value `lval`. This example clicks on a link element with the
        partial text `"news"`:

        ```python
        click_link("partial_text", "news")
        ```

        Arguments/return value:

        `ltype`: The link selector type.

        `lval`: The value of the link selector type.

        `index`: Choose from the list of matching elements (default 0).

        """

        # The implementation of the action
        def click_link(ltype: LinkType, lval: str, index: int = 0):
            self._api_map["link"][ltype](lval)[index].click()

        # Return the action
        return click_link

    @webaction
    def _mk_is_present(self) -> ActionFunc:
        R"""Action `is_present` checks if a web element is present

        The action `is_present` checks if a web element based on a
        selector type and the value of such a selector is present.
        This example returns `True` if a web element with id `"news"`
        is present:

        ```python
        is_present("id", "news")
        ```

        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `wait_time`: How long to wait for the web element to be
        present (default `None`).

        `return`: Returns True if the web element is present.

        """

        # The implementation of the action
        def is_present(
                stype: SelectorType,
                sval: str,
                wait_time: int | None = None) -> bool:
            if stype in self._api_map["is_element_present"]:
                return self._api_map["is_element_present"][stype](
                    sval, wait_time=wait_time)
            else:
                raise WebInteractionError(
                    f'Action is_element_present failed: ' + \
                    f'Unknown element type: {stype}\n')

        # Return the action
        return is_present
    
    @webaction
    def _mk_is_not_present(self) -> ActionFunc:
        R"""Action `is_not_present` checks if a web element is not present

        The action `is_not_present` checks if a web element based on
        the selector type `stype` with the value `sval` is not
        present. This example returns `True` if a web element with
        name `"loginform"` is not present:

        ```python
        is_not_present("name", "loginform")
        ```
        
        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `wait_time`: How long to wait for the web element to be
        present (default `None`)

        `return`: Returns True if the web element is not present.

        """

        # The implementation of the action
        def is_not_present(
                stype: SelectorType,
                sval: str,
                wait_time: int | None = None) -> bool:
            if stype in self._api_map["is_element_not_present"]:
                return self._api_map["is_element_not_present"][stype](
                    sval, wait_time=wait_time)
            else:
                raise WebInteractionError(
                    f'Action is_element_not_present failed: ' + \
                    f'Unknown element type: {stype}\n')

        # Return the action
        return is_not_present
    
    @webaction
    def _mk_is_text_present(self) -> ActionFunc:
        R"""Action `is_text_present` checks if a text is present

        The action `is_text_present` checks if the text is
        present. This example returns `True` if the text `"Login
        succeeded"` is present within 3 seconds:

        ```python
        is_text_present("Login succeeded", 3)
        ```

        Arguments/return value:

        `text`: The text to find.

        `wait_time`: How long to wait for the text to be present
        (default `None`).

        `return`: Returns `True` if the text is present.

        """
        
        # The implementation of the action
        def is_text_present(
                text: str,
                wait_time: int | None = None) -> bool:
            return self._browser.is_text_present(text, wait_time)

        # Return the action
        return is_text_present

    @webaction
    def _mk_is_text_not_present(self) -> ActionFunc:
        R"""Action `is_text_present` checks if a text is not present

        The action `is_text_not_present` checks if the text is not
        present. This example returns `True` if the text `"Login
        failed"` is not present:

        ```python
        is_text_not_present("Login failed")
        ```

        Arguments/return value:

        `text`: The text that should't be present.

        `wait_time`: How long to wait for the text to be present
        (default `None`).

        `return`: Returns `True` if the text is not present.

        """

        # The implementation of the action
        def is_text_not_present(
                text: str,
                wait_time: int | None = None) -> bool:
            return not self._browser.is_text_present(text, wait_time)

        # Return the action
        return is_text_not_present

    @webaction
    def _mk_element_attach_file(self) -> ActionFunc:
        R"""Action `element_attach_file` attachs a file to the web element

        Attach a file to the web element (a file input element).

        Arguments/return value:

        `file_path`: Absolute path to file.
        
        `element`: A list of the web elements.

        `index`: Choose from the list of matching elements (default `None`).

        """

        # The implementation of the action
        def attach_file(element: ElementList, index: int | None = None):
            _element_action_it(element, index, "fill", file_path)
        
        # Return the action
        return attach_file

    @webaction
    def _mk_attach_file(self) -> ActionFunc:
        R"""Action `attach_file` attachs a file to a web element

        Attach a file to a web element (a file input element).  In
        this example, the file `"/path/to/file"` is attached to a web
        element with the name `"thefile"`:

        ```python
        attach_file("/path/to/file", "name", "thefile")
        ```

        Arguments/return value:

        `file_path`: Absolute path to file.
        
        `stype`: The selector type.

        `sval`: The value of the selector type.

        `index`: Choose from the list of matching elements (default `None`).

        """

        # The implementation of the action
        def attach_file(
                file_path: str,
                stype: SelectorType, sval: str, index: int | None = None):
            element = self._do_find(stype, sval)
            _element_action_it(element, index, "fill", file_path)
        
        # Return the action
        return attach_file

    @webaction
    def _mk_element_doall(self) -> ActionFunc:
        R"""Do action on all elements of list of the web elements

        Do the same action on all web elements in the web element
        list. In this example, all chekboxes on a web page are
        checked:

        ```python
        element_doall( \
          find("xpath", "//input[@type='checkbox']"), element_check)
        ```

        Arguments/return value:

        `elements`: A list of the web elements.

        `action`: The action performed on each element.

        `*args`: Arguments to the action.

        `**kw`: Keyword arguments to the action.

        `return`: The aggregated result of all actions or None.

        """

        # The implementation of the action
        def element_doall(
                elements: ElementList,
                action: ActionFunc,
                *args: tuple, sep: str = "\n", **kw: dict) -> str | None:
            elements = _prep_element(elements)
            kw['element'] = elements
            result = ""
            for i in range(len(elements)):
                kw['index'] = i
                r =  _do_inline_action(action(*args, **kw))
                if r:
                    if result:
                        result += sep
                    result += r
            if result: return result

        # Return the action
        return element_doall
    
    @webaction
    def _mk_doall(self) -> ActionFunc:
        R"""Do action on all elements of list of web elements

        Do the same action on all web elements in the web element
        list. In this example, the value of all select elements on a
        web page are fetched:

        ```python
        doall("tag", "select", element_get_value)
        ```

        Arguments/return value:

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `action`: The action performed on each element.

        `*args`: Arguments to the action.

        `sep`: A separator inserted between each of result returned if
        the action returns a result (default `"\n"`)

        `**kw`: Keyword arguments to the action.

        `return`: The aggregated result of all actions or None.

        """

        # The implementation of the action
        def doall(
                stype: SelectorType, sval: str,
                action: ActionFunc,
                *args: tuple, sep: str = "\n", **kw: dict) -> str | None:
            #elements = _prep_element(self._do_find(stype, sval))
            elements = self._do_find(stype, sval)
            kw['element'] = elements
            result = ""
            for i in range(len(elements)):
                kw['index'] = i
                r =  _do_inline_action(action(*args, **kw))
                if r:
                    if result:
                        result += sep
                    result += r
            if result: return result

        # Return the action
        return doall

    @webaction
    def _mk_element_cond(self) -> ActionFunc:
        R"""Do action on the web element if condition is true

        Do `ifaction` if condition is true. If provided, do elseaction
        if condition is false.  This example unchecks the checkbox
        element `checkbox1` if it is checked, and checks if it is
        unchecked (a checkbox toggle):

        ```python
        element_cond( \
          verify(element_get_value(checkbox1), "on", assert_it = False), \
          checkbox1, ifaction = element_uncheck, elseaction = element_check)
        ```
        
        Arguments/return value:

        `condition`: The condition.

        `element`: A list of the web elements.

        `ifaction`: The action performed if condition is true.

        `*args`: Arguments to the action (both `ifaction` and `elseaction`).

        `elseaction`: The action performed if condition is false
        (default `None`).

        `index`: Choose from the list of matching elements (default 0).
        
        `**kw`: Keyword arguments to the action (both `ifaction` and
        `elseaction`).

        `return`: The aggregated result of all actions or None.

        """

        # The implementation of the action
        def element_cond(
                condition: bool,
                element: ElementList,
                ifaction: ActionFunc,
                *args: tuple,
                elseaction: ActionFunc = None,
                index: int = 0, **kw: dict) -> str | None:
            condition = _prep_element(condition)
            kw['element'] = _prep_element(element)
            kw['index'] = index
            if condition:
                return _do_inline_action(ifaction(*args, **kw))
            elif elseaction:
                return _do_inline_action(elseaction(*args, **kw))

        # Return the action
        return element_cond
    
    @webaction
    def _mk_cond(self) -> ActionFunc:
        R"""Do action a web element if condition is true

        Do `ifaction` if condition is true. If provided, do elseaction
        if condition is false.  This example unchecks the checkbox
        element `checkbox1` if it is checked, and checks if it is
        unchecked (a checkbox toggle):

        ```python
        cond( \
          verify(element_get_value(checkbox1), "on", assert_it = False), \
          "name", "checkbox1", \
          ifaction = element_uncheck, elseaction = element_check)
        ```

        Arguments/return value:

        `condition`: The condition.

        `stype`: The selector type.

        `sval`: The value of the selector type.

        `ifaction`: The action performed if condition is true.

        `*args`: Arguments to the action (both `ifaction` and `elseaction`).

        `elseaction`: The action performed if condition is false
        (default `None`).

        `index`: Choose from the list of matching elements (default 0).
        
        `**kw`: Keyword arguments to the action (both `ifaction` and
        `elseaction`).

        `return`: The aggregated result of all actions or None.

        """

        # The implementation of the action
        def cond(
                condition: bool,
                stype: SelectorType, sval: str,
                ifaction: ActionFunc,
                *args: tuple,
                elseaction: ActionFunc = None,
                index: int = 0, **kw: dict) -> str | None:
            condition = _prep_element(condition)
            kw['element'] = self._do_find(stype, sval)
            kw['index'] = index
            if condition:
                return _do_inline_action(ifaction(*args, **kw))
            elif elseaction:
                return _do_inline_action(elseaction(*args, **kw))

        # Return the action
        return cond

    
#
# Some more help functions (to produce documentation)
#
    
def _insert_sig_in_doc(
        method: Callable,
        doc: str | None = None,
        name: str | None = None,
        clsname: str = "") -> str:
    R"""Insert the signature of the method into method documentation

    The function modifies the documentation string by inserting the
    signature of the method. The modified documentation string is
    returned.

    Arguments/return value:

    `method`: The method

    `doc`: The documentation string of the method (optional, grap the
    documentation string from the method if it is not provided)

    `name`: The name of the method (optional, grap the name of the
    method from the method itself if it is not provided)

    `clsname`: The class name (optional)
    
    `return`: Modified documentation string

    """
    if not doc: doc = method.__doc__
    doc_lines = doc.splitlines()
    sig = str(signature(method))
    if ", " in sig:
        sig = sig.replace("self, ", "", 1)
    else:
        sig = sig.replace("self", "", 1)
    if not name: name = method.__name__
    if clsname:
        clsname += "."
        ttxt = "Method"
    else:
        ttxt = "Class"
    res = f"{ttxt} `{clsname}{name}`\n"
    res += f"\n`{name}{sig}`\n\n*{doc_lines[0]}*\n"
    res += "\n".join(doc_lines[1:])
    return res


def _get_action_name_and_sig(mkf: Callable) -> tuple:
    R"""Get the name and signature of an action

    Get the name and the signature of an action by using the method
    making the action and then grap this information from the action
    itself.
    
    Arguments/return value:

    `mkf`: Method making the specific action

    `return`: A two-tuple with the name and signature of the action

    """
    f = mkf(None)
    fname = f["name"]
    fsig = str(signature(f["action"]))
    return fname, fsig


def _get_actions() -> dict:
    R"""Get information about all actions

    Get the information about all actions, including their name,
    signature, the method making the action, and the qualified name of
    the method making the action.

    Return value:

    `return`: A dictionary where the key is the action name and the
    content of each item is another dictionary with the information
    about that action.

    """

    # Traverse the complete content of the `WebInteraction` class
    actions = {}
    for a in dir(WebInteraction):

        # If this is a method making an action
        if a[:4] == '_mk_':

            # Get the method
            mkf = getattr(WebInteraction, a)

            # Check that it is not deprecated
            if mkf.__doc__[:10] == "DEPRECATED":
                continue

            # Get the name of the action and its signature
            fname, fsig = _get_action_name_and_sig(mkf)

            # Use the name as a key to a dictionary item with the
            # method making the action, the signature of the action
            # and the qualified name of the method making the action
            actions[fname] = {
                "mkf": mkf,
                "sig": fsig,
                "mk_name": mkf.__qualname__}

    # Return the information about all actions
    return actions


def _make_docs() -> dict:
    """Create action documentation

    Traverese the `WebInteraction` class for each action and generates
    its documentation.

    Return value:

    `return`: A dictionary with the documentation of each named action

    """
    docs = {}
    for fname, finfo in _get_actions().items():
        docs[fname] = fname + finfo["sig"] + "\n\n"
        docs[fname] += finfo["mkf"].__doc__.strip()
    return docs

def _action_list(mk_name: bool = True) -> list:
    R"""List all names of the actions

    List all names of the actions, and ensure that the actions related
    are neigbours in the list.

    Arguments/return value:

    `mk_name`: If `mk_name` is `True` the names in the list is the
    qualified names of the methods making the actions, and not the
    name of the actions them self (default `True`)

    `return`: A list of the names of all actions (or the qualified
    name of the methods making the actions)

    """

    # get all actions
    actions = _get_actions()

    # Should we return the action names or the qualified name of the
    # methods making the actions
    if mk_name:
        get_name = lambda a: actions[a]["mk_name"]
    else:
        get_name = lambda a: a

    # First, list all names that does not start with `"element_"`
    action_names = actions.keys()
    main_a = [k for k in filter(lambda x: not "element_" in x, action_names)]

    # Then insert the names that start with `"element_"` at the right position
    action_list = []
    for a in main_a:
        action_list.append(get_name(a))
        if f"element_{a}" in action_names:
            action_list.append(get_name(f"element_{a}"))

    # Return the list of names
    return action_list
 
def _list_actions(docs, sep: str = "\n", pre: str = "") -> str:
    R"""List all actions

    List all action name in a text string, one action per line.

    Arguments/return value:

    `sep`: The separator between each action name (default a new line, `"\n"`)

    `pre`: A string put in front of each name (default the empty string `""`)

    `return`: The text string with all the action names

    """
    actions = _action_list(mk_name = False)
    return sep.join([pre + key for key in actions])


################################################################################
#
# Use the module as a program (to perform a web interaction script)
#
# Usage: webinteract [-h] [-V] [-n NAME_SPACE] [-s PW_SERVICE] [-a PW_ACCOUNT]
#                    [-o OUTPUT_FILE_NAME] [--driver DRIVER] [--headless]
#                    [--keep] [--prompt PROMPT] [--prompt-color PROMPT_COLOR]
#                    [--prompt-output-color PROMPT_OUTPUT_COLOR]
#                    [--prompt-alert-color PROMPT_ALERT_COLOR]
#                    [script]
#
# Perform a web interaction action script.
#
# Positional arguments:
#   script                The web script file name (path), '-' for stdin
#
# Options:
#   -h, --help            Show this help message and exit
#   -V, --version         Show program's version number and exit
#   -D, --doc [ACTION]    Print documentation of module or specific action
#   -n, --name-space NAME_SPACE
#                         Add variables to name space (json)
#   -s, --pw-service PW_SERVICE
#                         Password stored at this service name in keychain
#   -a, --pw-account PW_ACCOUNT
#                         Password stored at this account name in keychain
#   -o, --output-file OUTPUT_FILE_NAME
#                         Any output is written to this file (default stdout)
#   --driver DRIVER       The web interaction driver name
#   --headless            Run browser headless (invisible)
#   --keep                Keep browser running after script has terminated
#   --prompt PROMPT       The text of the interactive prompt
#   --prompt-color PROMPT_COLOR
#                         The text color of the interactive prompt
#   --prompt-output-color PROMPT_OUTPUT_COLOR
#                         The text color of the interactive output
#   --prompt-alert-color PROMPT_ALERT_COLOR
#                         The text color of the interactive alerts
# 
################################################################################


def main():
    """Run module as a program

    Run the module as a program, either interpreting a given wia
    script or with a command prompt.

    """

    # Needs these modules
    import traceback

    # Create argument parser
    import argparse, json
    parser = argparse.ArgumentParser(
        description="Perform a web interaction action script.")
    parser.add_argument("-V", "--version", action="version",
                        version=f"%(prog)s " + version)
    parser.add_argument("-D", "--doc", nargs='?', metavar="ACTION",
                        const=True, default=None,
                        help="print documentation of module or specific action")
    parser.add_argument("-d", "--doc-noactions", action="store_true",
                        default=False, help=argparse.SUPPRESS)
    parser.add_argument("-L", "--list-actions", nargs='?', metavar="ACTION",
                        const=True, default=None, help=argparse.SUPPRESS)
    parser.add_argument("-M", "--list-methods", nargs='?', metavar="METHOD",
                        const=True, default=None, help=argparse.SUPPRESS)
    parser.add_argument("-n", "--name-space", type=json.loads,
                        help="add variables to name space (json)")
    parser.add_argument("-s", "--pw-service",
                        help="password stored at this service name in keychain")
    parser.add_argument("-a", "--pw-account",
                        help="password stored at this account name in keychain")
    parser.add_argument("-o", "--output-file", dest="output_file_name",
                        help="any output is written to this file " +
                        "(default stdout)")
    parser.add_argument("--driver", default=None,
                        help="the web interaction driver name")
    parser.add_argument("--headless", action="store_true",
                        help="run browser headless (invisible)")
    parser.add_argument("--keep", action="store_true",
                        help="keep browser running after script has terminated")
    parser.add_argument("--prompt", default="wia> ",
                        help="the text of the interactive prompt")
    parser.add_argument("--prompt-color", default="LIGHTYELLOW_EX",
                        help="the text color of the interactive prompt")
    parser.add_argument("--prompt-output-color", default="GREEN",
                        help="the text color of the interactive output")
    parser.add_argument("--prompt-alert-color", default="LIGHTRED_EX",
                        help="the text color of the interactive alerts")
    parser.add_argument("script", nargs='?',
                        help="the web script file name (path), '-' for stdin")
    
    # Parse arguments
    args = parser.parse_args()

    # List all web actions (for internal usage)
    if args.list_actions:
        docs = _make_docs()
        if args.list_actions == True:
            print(_list_actions(docs))
        elif args.list_actions in docs:
            fname, fsig = _get_action_name_and_sig(
                getattr(WebInteraction, "_mk_" + args.list_actions))
            print(fname + fsig)
        else:
            print(f"List actions: Unknown action '{args.list_actions}'")
            sys.exit(1)
        return

    # List `WebInteraction` methods
    _methods = ["WebInteraction", "update", "execute", "quit"]
    if args.list_methods:
        if args.list_methods == True:
            print("\n".join(_methods))
        elif args.list_methods in _methods:
            if args.list_methods == "WebInteraction":
                _m = getattr(WebInteraction, "__init__")
            else:
                _m = getattr(WebInteraction, args.list_methods)
            sig = str(signature(_m))
            # Remove "self"
            if ", " in sig: 
                sig = sig.replace("self, ", "", 1)
            else:
                sig = sig.replace("self", "", 1)
            print(args.list_methods + sig)
        else:
            print(f"List methods: Unknown method '{args.list_methods}'")
            sys.exit(1)
        return
    
    # Print documentation?
    if args.doc_noactions: args.doc = True
    if args.doc:
        docs = _make_docs()
        if args.doc == True:
            print('\n' + __doc__.strip() + '\n')
            print(_insert_sig_in_doc(
                WebInteraction.__init__,
                WebInteraction.__doc__,
                "WebInteraction"))
            print("\n".join(WebInteraction.__init__.__doc__.splitlines()[2:]))
            for _m in _methods[1:]:
                print(_insert_sig_in_doc(
                    getattr(WebInteraction, _m),
                    clsname="WebInteraction"))
            if not args.doc_noactions:
                print("All available web actions:\n")
                print(_list_actions(docs, pre=" - ") + '\n')
        elif args.doc in docs:
            print('\n' + docs[args.doc] + '\n')
        else:
            print(f"Print documentation: Unknown action '{args.doc}'")
        return

    # Create and update name space from arguments
    ns = {}
    if args.name_space:
        ns.update(args.name_space)

    # Open output file
    if args.output_file_name:
        args.output_file = open(args.output_file_name, "w")
    else:
        args.output_file = sys.stdout

    # Get password from keychain?
    if args.pw_account or args.pw_service:
        if args.pw_account and args.pw_service:
            import keyring
            if "pw_service" not in ns:
                ns["pw_service"] = args.pw_service
            if "pw_account" not in ns:
                ns["pw_account"] = args.pw_account
            ns["pw"] = keyring.get_password(args.pw_service, args.pw_account)
            if not ns["pw"]:
                raise LookupError(
                    f"Didn't find pw for {args.pw_account} in keychain")
        else:
            raise LookupError(
                f"Needs both --pw-service and --pw-account to fetch pw")

    # Perform the web interaction
    try:

        # Create the keyword arguments to `WebInteraction` class
        # (equal to the `Browser` class of `splinter`)
        kw = {"headless": args.headless}
        if args.driver:
            kw["driver_name"] = args.driver

        # Create the web interaction object
        web_interaction = WebInteraction(**kw)

        # Update the namespace of the web interaction script
        web_interaction.update(ns)

        # Perform the script in the file with file name args.script
        if args.script and args.script != "-":
            input = open(args.script)
        else:
            input = None
        web_interaction(input, vars(args))
        
    # Something else failed in the web-page interaction
    except Exception as err:
        msg = f"Web page interaction failed: {err}\n"
        msg += "".join(traceback.format_exception(*sys.exc_info()))
        raise WebInteractionError(msg)

    # Quit browser (if it was successfully launched)
    if not args.keep:
        try:
            web_interaction.quit()
        except:
            pass


# Execute this module as a program
if __name__ == '__main__':
    main()