Source code for guibot.config

# Copyright 2013-2018 Intranet AG and contributors
#
# guibot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# guibot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with guibot.  If not, see <http://www.gnu.org/licenses/>.

"""
Global and local (per target or region instance) configuration.

SUMMARY
------------------------------------------------------


INTERFACE
------------------------------------------------------

"""

import logging
from typing import Any

from .errors import *

log = logging.getLogger("guibot.config")


class GlobalConfig(type):
    """
    Metaclass used for the definition of static properties (the settings).

    We overwrite the name of the class in order to avoid documenting
    all settings here and adding an empty actual class. Instead, the resulting
    documentation contains just the config class (using this as metaclass)
    and all settings respectively. In this way the front user should not worry
    about such implementation detail and simply use the provided properties.

    For those that like to think about it nonetheless: All methods of the
    resulting config class are therefore static since they are methods of
    a class object, i.e. a metaclass instance.
    """

    # operational parameters shared between all instances
    _drag_delay = 0.5
    _drop_delay = 0.5
    _rescan_speed_on_find = 0.2
    _wait_for_animations = False
    _smooth_mouse_drag = True
    _screen_autoconnect = True
    _preprocess_special_chars = True
    _save_needle_on_error = True
    _image_logging_level = logging.ERROR
    _image_logging_destination = "imglog"
    _image_logging_step_width = 3
    _image_quality = 3

    # backends shared between all instances
    _display_control_backend = "autopy"
    _find_backend = "hybrid"
    _contour_threshold_backend = "adaptive"
    _template_match_backend = "ccoeff_normed"
    _feature_detect_backend = "ORB"
    _feature_extract_backend = "ORB"
    _feature_match_backend = "BruteForce-Hamming"
    _text_detect_backend = "contours"
    _text_ocr_backend = "pytesseract"
    _deep_learn_backend = "pytorch"
    _hybrid_match_backend = "template"

    def delay_after_drag(cls, value: float = None) -> float | None:
        """
        Get or set property attribute.

        :param value: timeout before drag operation
        :returns: current value if no argument was passed otherwise None
        """
        if value is None:
            return cls._drag_delay
        else:
            cls._drag_delay = value
            return None

    #: timeout before drag operation
    delay_after_drag = property(fget=delay_after_drag, fset=delay_after_drag)

    def delay_before_drop(cls, value: float = None) -> float | None:
        """
        Get or set property attribute.

        :param value: timeout before drop operation
        :returns: current value if no argument was passed otherwise None
        """
        if value is None:
            return cls._drop_delay
        else:
            cls._drop_delay = value
            return None

    #: timeout before drop operation
    delay_before_drop = property(fget=delay_before_drop, fset=delay_before_drop)

    def rescan_speed_on_find(cls, value: float = None) -> float | None:
        """
        Get or set property attribute.

        :param value: time interval between two image matching attempts
                      (used to reduce overhead on the CPU)
        :returns: current value if no argument was passed otherwise None
        """
        if value is None:
            return cls._rescan_speed_on_find
        else:
            cls._rescan_speed_on_find = value
            return None

    #: time interval between two image matching attempts (used to reduce overhead on the CPU)
    rescan_speed_on_find = property(
        fget=rescan_speed_on_find, fset=rescan_speed_on_find
    )

    def wait_for_animations(cls, value: bool = None) -> bool | None:
        """
        Getter/setter for property attribute.

        :param value: whether to wait for animations to complete and match only static (not moving) targets
        :returns: current value if no argument was passed otherwise None
        :raises: :py:class:`ValueError` if value is not boolean or None

        This is useful to handle highly animated environments with lots of moving
        targets where it might be inappropriate to click on a target until it stops
        and the corresponding animation has finished.
        """
        if value is None:
            return cls._wait_for_animations
        elif value is True or value is False:
            cls._wait_for_animations = value
            return None
        else:
            raise ValueError

    #: whether to wait for animations to complete and match only static (not moving) targets
    wait_for_animations = property(fget=wait_for_animations, fset=wait_for_animations)

    def smooth_mouse_drag(cls, value: bool = None) -> bool | None:
        """
        Getter/setter for property attribute.

        :param value: whether to move the mouse cursor to a location instantly or smoothly
        :returns: current value if no argument was passed otherwise None
        :raises: :py:class:`ValueError` if value is not boolean or None

        This is useful if a routine task has to be executed faster without
        supervision or the need of debugging.
        """
        if value is None:
            return cls._smooth_mouse_drag
        elif value is True or value is False:
            cls._smooth_mouse_drag = value
            return None
        else:
            raise ValueError

    #: whether to move the mouse cursor to a location instantly or smoothly
    smooth_mouse_drag = property(fget=smooth_mouse_drag, fset=smooth_mouse_drag)

    def preprocess_special_chars(cls, value: bool = None) -> bool | None:
        """
        Getter/setter for property attribute.

        :param value: whether to preprocess capital and special characters and
                      handle them internally
        :returns: current value if no argument was passed otherwise None

        .. warning:: The characters will be forcefully preprocessed for the
            autopy on linux (capital and special) and vncdotool (capital) backends.
        """
        if value is None:
            return cls._preprocess_special_chars
        elif value is True or value is False:
            cls._preprocess_special_chars = value
            return None
        else:
            raise ValueError

    #: whether to preprocess capital and special characters and handle them internally
    preprocess_special_chars = property(
        fget=preprocess_special_chars, fset=preprocess_special_chars
    )

    def save_needle_on_error(cls, value: bool = None) -> bool | None:
        """
        Getter/setter for property attribute.

        :param value: whether to perform an extra needle dump on matching error
        :returns: current value if no argument was passed otherwise None
        """
        if value is None:
            return cls._save_needle_on_error
        elif value is True or value is False:
            cls._save_needle_on_error = value
            return None
        else:
            raise ValueError

    #: whether to perform an extra needle dump on matching error
    save_needle_on_error = property(
        fget=save_needle_on_error, fset=save_needle_on_error
    )

    def image_logging_level(cls, value: int = None) -> int | None:
        """
        Getter/setter for property attribute.

        :param value: logging level similar to the python logging module
        :returns: current value if no argument was passed otherwise None

        .. seealso:: See the image logging documentation for more details.
        """
        if value is None:
            return cls._image_logging_level
        else:
            cls._image_logging_level = value
            return None

    #: logging level similar to the python logging module
    image_logging_level = property(fget=image_logging_level, fset=image_logging_level)

    def image_logging_step_width(cls, value: int = None) -> int | None:
        """
        Getter/setter for property attribute.

        :param value: number of digits when enumerating the image
                      logging steps, e.g. value=3 for 001, 002, etc.
        :returns: current value if no argument was passed otherwise None
        """
        if value is None:
            return cls._image_logging_step_width
        else:
            cls._image_logging_step_width = value
            return None

    #: number of digits when enumerating the image logging steps, e.g. value=3 for 001, 002, etc.
    image_logging_step_width = property(
        fget=image_logging_step_width, fset=image_logging_step_width
    )

    def image_quality(cls, value: int = None) -> int | None:
        """
        Getter/setter for property attribute.

        :param value: quality of the image dumps ranging from 0 for no compression
                      to 9 for maximum compression (used to save space and reduce
                      the disk space needed for image logging)
        :returns: current value if no argument was passed otherwise None
        """
        if value is None:
            return cls._image_quality
        else:
            cls._image_quality = value
            return None

    #: quality of the image dumps ranging from 0 for no compression to 9 for maximum compression
    # (used to save space and reduce the disk space needed for image logging)
    image_quality = property(fget=image_quality, fset=image_quality)

    def image_logging_destination(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: relative path of the image logging steps
        :returns: current value if no argument was passed otherwise None
        """
        if value is None:
            return cls._image_logging_destination
        else:
            cls._image_logging_destination = value
            return None

    #: relative path of the image logging steps
    image_logging_destination = property(
        fget=image_logging_destination, fset=image_logging_destination
    )

    def display_control_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the display control backend
        :returns: current value if no argument was passed otherwise None
        :raises: :py:class:`ValueError` if value is not among the supported backends

        Supported backends:
           * autopy - Windows, Linux, and OS X compatible with both the GUI
                      actions and their calls executed on the same machine.
           * pyautogui - Windows, Linux, and OS X compatible with both the GUI
                         actions and their calls executed on the same machine
           * vncdotool - guest OS independent or Linux remote OS with GUI
                         actions on a remote machine through vnc and their
                         calls on a vnc client machine.
           * xdotool - Linux X server compatible with both the GUI
                       actions and their calls executed on the same machine.
           * qemu - guest OS independent with GUI actions on a virtual machine
                    through Qemu Monitor object (provided by Autotest) and
                    their calls on the host machine.

        .. warning:: To use a particular backend you need to satisfy its dependencies,
            i.e. the backend has to be installed or you will have unsatisfied imports.
        """
        if value is None:
            return cls._display_control_backend
        else:
            if value not in ["autopy", "xdotool", "vncdotool", "qemu", "pyautogui"]:
                raise ValueError("Unsupported backend for GUI actions '%s'" % value)
            cls._display_control_backend = value
            return None

    #: name of the display control backend
    display_control_backend = property(
        fget=display_control_backend, fset=display_control_backend
    )

    # these methods do not check for valid values since this
    # is already done during region and target initialization
    def find_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the computer vision backend
        :returns: current value if no argument was passed otherwise None

        Supported backends:
            * autopy - simple bitmap matching provided by AutoPy
            * contour - contour matching using overall shape estimation
            * template - template matching using correlation coefficients,
                         square difference, etc.
            * feature - matching using a mixture of feature detection,
                        extraction and matching algorithms
            * cascade - matching using OpenCV pretrained Haar cascades
            * text - text matching using EAST, ERStat, or custom text detection,
                     followed by Tesseract or Hidden Markov Model OCR
            * tempfeat - a mixture of template and feature matching where the
                         first is used as necessary and the second as sufficient stage
            * deep - deep learning matching using convolutional neural network but
                     customizable to any type of deep neural network
            * hybrid - use a composite approach with any of the above methods
                       as matching steps in a fallback sequence

        .. warning:: To use a particular backend you need to satisfy its dependencies,
            i.e. the backend has to be installed or you will have unsatisfied imports.
        """
        if value is None:
            return cls._find_backend
        else:
            cls._find_backend = value
            return None

    #: name of the computer vision backend
    find_backend = property(fget=find_backend, fset=find_backend)

    def contour_threshold_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the contour threshold backend
        :returns: current value if no argument was passed otherwise None

        Supported backends: normal, adaptive, canny.
        """
        if value is None:
            return cls._contour_threshold_backend
        else:
            cls._contour_threshold_backend = value
            return None

    #: name of the contour threshold backend
    contour_threshold_backend = property(
        fget=contour_threshold_backend, fset=contour_threshold_backend
    )

    def template_match_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the template matching backend
        :returns: current value if no argument was passed otherwise None

        Supported backends: autopy, sqdiff, ccorr, ccoeff, sqdiff_normed,
        ccorr_normed, ccoeff_normed.
        """
        if value is None:
            return cls._template_match_backend
        else:
            cls._template_match_backend = value
            return None

    #: name of the template matching backend
    template_match_backend = property(
        fget=template_match_backend, fset=template_match_backend
    )

    def feature_detect_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the feature detection backend
        :returns: current value if no argument was passed otherwise None

        Supported  backends: BruteForce, BruteForce-L1, BruteForce-Hamming,
        BruteForce-Hamming(2), in-house-raw, in-house-region.
        """
        if value is None:
            return cls._feature_detect_backend
        else:
            cls._feature_detect_backend = value
            return None

    #: name of the feature detection backend
    feature_detect_backend = property(
        fget=feature_detect_backend, fset=feature_detect_backend
    )

    def feature_extract_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the feature extraction backend
        :returns: current value if no argument was passed otherwise None

        Supported backends: ORB, FAST, STAR, GFTT, HARRIS, Dense, oldSURF.
        """
        if value is None:
            return cls._feature_extract_backend
        else:
            cls._feature_extract_backend = value
            return None

    #: name of the feature extraction backend
    feature_extract_backend = property(
        fget=feature_extract_backend, fset=feature_extract_backend
    )

    def feature_match_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the feature matching backend
        :returns: current value if no argument was passed otherwise None

        Supported backends: ORB, BRIEF, FREAK.
        """
        if value is None:
            return cls._feature_match_backend
        else:
            cls._feature_match_backend = value
            return None

    #: name of the feature matching backend
    feature_match_backend = property(
        fget=feature_match_backend, fset=feature_match_backend
    )

    def text_detect_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the text detection backend
        :returns: current value if no argument was passed otherwise None

        Supported backends: east, erstat, contours, components.
        """
        if value is None:
            return cls._text_detect_backend
        else:
            cls._text_detect_backend = value
            return None

    #: name of the text detection backend
    text_detect_backend = property(fget=text_detect_backend, fset=text_detect_backend)

    def text_ocr_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the optical character recognition backend
        :returns: current value if no argument was passed otherwise None

        Supported backends: pytesseract, tesserocr, tesseract (OpenCV), hmm, beamSearch.
        """
        if value is None:
            return cls._text_ocr_backend
        else:
            cls._text_ocr_backend = value
            return None

    #: name of the optical character recognition backend
    text_ocr_backend = property(fget=text_ocr_backend, fset=text_ocr_backend)

    def deep_learn_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the deep learning backend
        :returns: current value if no argument was passed otherwise None

        Supported backends: pytorch, tensorflow (partial).
        """
        if value is None:
            return cls._deep_learn_backend
        else:
            cls._deep_learn_backend = value
            return None

    #: name of the deep learning backend
    deep_learn_backend = property(fget=deep_learn_backend, fset=deep_learn_backend)

    def hybrid_match_backend(cls, value: str = None) -> str | None:
        """
        Getter/setter for property attribute.

        :param value: name of the hybrid matching backend for unconfigured one-step targets
        :returns: current value if no argument was passed otherwise None

        Supported backends: all nonhybrid backends of :py:func:`GlobalConfig.find_backend`.
        """
        if value is None:
            return cls._hybrid_match_backend
        else:
            cls._hybrid_match_backend = value
            return None

    #: name of the hybrid matching backend for unconfigured one-step targets
    hybrid_match_backend = property(
        fget=hybrid_match_backend, fset=hybrid_match_backend
    )


[docs] class GlobalConfig(object, metaclass=GlobalConfig): # type: ignore """ Handler for default configuration present in all cases where no specific value is set. The methods of this class are shared among all of its instances. """ pass
[docs] class TemporaryConfig(object): """ Proxy a GlobalConfig instance extending it to add context support. The context support is such that once this context ends the changes to the wrapped config object are restored. This is useful when we have a global config instance and need to change it only for a few operations. :: >>> print(GlobalConfig.delay_before_drop) 0.5 >>> with TemporaryConfig() as cfg: ... cfg.delay_before_drop = 1.3 ... print(cfg.delay_before_drop) ... print(GlobalConfig.delay_before_drop) ... 1.3 1.3 >>> print(GlobalConfig.delay_before_drop) 0.5 """ def __init__(self) -> None: """Build a temporary global config.""" object.__setattr__(self, "_original_values", {}) def __getattribute__(self, name: Any) -> Any: """Get attribute given a name.""" # fallback to GlobalConfig return getattr(GlobalConfig, name) def __setattr__(self, name: Any, value: Any) -> None: """Set attribute given a name and a value.""" original_values = object.__getattribute__(self, "_original_values") # store the original value only at the first set operation, # so further changes won't overwrite the history if name not in original_values: original_values[name] = getattr(GlobalConfig, name) setattr(GlobalConfig, name, value) def __enter__(self) -> "TemporaryConfig": """Set up context manager upon entry.""" # our temporary config object return self def __exit__(self, *_: tuple[type, ...]) -> None: """Clean up context manager upon exit.""" original_values = object.__getattribute__(self, "_original_values") # restore original configuration values for name, value in original_values.items(): setattr(GlobalConfig, name, value) # no need to keep the backup once everything has been restored original_values.clear()
[docs] class LocalConfig(object): """ Contain locally the configuration of all display control and computer vision backends. The local container is reponsible for making them behave according to the selected parameters as well as for providing information about them and the current parameters. """ def __init__(self, configure: bool = True, synchronize: bool = True) -> None: """ Build a container for the entire backend configuration. :param configure: whether to also generate configuration :param synchronize: whether to also apply configuration Available algorithms can be seen in the `algorithms` attribute whose keys are the algorithm types and values are the members of these types. The algorithm types are shortened as `categories`. A parameter can be accessed as follows (example):: print(self.params["control"]["vnc_hostname"]) """ self.categories = {} self.algorithms = {} self.params = {} self.categories["type"] = "backend_types" self.algorithms["backend_types"] = ("cv", "dc") # other attributes from .imagelogger import ImageLogger self.imglog = ImageLogger() self.imglog.log = self.log if configure: self.__configure_backend() if synchronize: self.__synchronize_backend() def __configure_backend( self, backend: str = None, category: str = "type", reset: bool = False ) -> None: if category != "type": raise UnsupportedBackendError( "Backend category '%s' is not supported" % category ) if reset: # reset makes no sense here since this is the base configuration pass if backend is None: backend = "cv" if backend not in self.algorithms[self.categories[category]]: raise UnsupportedBackendError( "Backend '%s' is not among the supported ones: " "%s" % (backend, self.algorithms[self.categories[category]]) ) self.params[category] = {} self.params[category]["backend"] = backend
[docs] def configure_backend( self, backend: str = None, category: str = "type", reset: bool = False ) -> None: """ Generate configuration dictionary for a given backend. :param backend: name of a preselected backend, see `algorithms[category]` :param category: category for the backend, see `algorithms.keys()` :param reset: whether to (re)set all parent configurations as well :raises: :py:class:`UnsupportedBackendError` if `backend` is not among the supported backends for the category (and is not `None`) or the category is not found """ self.__configure_backend(backend, category, reset)
[docs] def configure(self, reset: bool = True, **kwargs: dict[str, type]) -> None: """ Generate configuration dictionary for all backends. :param reset: whether to (re)set all parent configurations as well If multiple categories are available and just some of them are configured, the rest will be reset to defaults. To configure specific category without changing others, use :py:func:`configure`. """ self.configure_backend(reset=reset)
def __synchronize_backend( self, backend: str = None, category: str = "type", reset: bool = False ) -> None: if category != "type": raise UnsupportedBackendError( "Backend category '%s' is not supported" % category ) if reset: # reset makes no sense here since this is the base configuration pass # no backend object to sync to backend = "cv" if backend is None else backend if backend not in self.algorithms[self.categories[category]]: raise UninitializedBackendError( "Backend '%s' has not been configured yet" % backend )
[docs] def synchronize_backend( self, backend: str = None, category: str = "type", reset: bool = False ) -> None: """ Synchronize a category backend with the equalizer configuration. :param backend: name of a preselected backend, see `algorithms[category]` :param category: category for the backend, see `algorithms.keys()` :param reset: whether to (re)sync all parent backends as well :raises: :py:class:`UnsupportedBackendError` if the category is not found :raises: :py:class:`UninitializedBackendError` if there is no backend object that is configured with and with the required name """ self.__synchronize_backend(backend, category, reset)
[docs] def synchronize( self, *args: tuple[type, ...], reset: bool = True, **kwargs: dict[str, type] ) -> None: """ Synchronize all backends with the current configuration dictionary. :param reset: whether to (re)sync all parent backends as well """ self.synchronize_backend(reset=reset)