# 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/>.
"""
SUMMARY
------------------------------------------------------
Global and local (per target or region instance) configuration.
INTERFACE
------------------------------------------------------
"""
import logging
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
_toggle_delay = 0.05
_click_delay = 0.1
_drag_delay = 0.5
_drop_delay = 0.5
_keys_delay = 0.2
_type_delay = 0.1
_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 = "pyautogui"
_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 toggle_delay(self, value=None):
"""
Getter/setter for property attribute.
:param value: time interval between mouse down and up in a click
:type value: float or None
:returns: current value if no argument was passed otherwise only sets it
:rtype: float or None
"""
if value is None:
return GlobalConfig._toggle_delay
else:
GlobalConfig._toggle_delay = value
#: time interval between mouse down and up in a click
toggle_delay = property(fget=toggle_delay, fset=toggle_delay)
def click_delay(self, value=None):
"""
Same as :py:func:`GlobalConfig.toggle_delay` but with
:param value: time interval after a click (in a double or n-click)
"""
if value is None:
return GlobalConfig._click_delay
else:
GlobalConfig._click_delay = value
#: time interval after a click (in a double or n-click)
click_delay = property(fget=click_delay, fset=click_delay)
def delay_after_drag(self, value=None):
"""
Same as :py:func:`GlobalConfig.toggle_delay` but with
:param value: timeout before drag operation
"""
if value is None:
return GlobalConfig._drag_delay
else:
GlobalConfig._drag_delay = value
#: timeout before drag operation
delay_after_drag = property(fget=delay_after_drag, fset=delay_after_drag)
def delay_before_drop(self, value=None):
"""
Same as :py:func:`GlobalConfig.toggle_delay` but with
:param value: timeout before drop operation
"""
if value is None:
return GlobalConfig._drop_delay
else:
GlobalConfig._drop_delay = value
#: timeout before drop operation
delay_before_drop = property(fget=delay_before_drop, fset=delay_before_drop)
def delay_before_keys(self, value=None):
"""
Same as :py:func:`GlobalConfig.toggle_delay` but with
:param value: timeout before key press operation
"""
if value is None:
return GlobalConfig._keys_delay
else:
GlobalConfig._keys_delay = value
#: timeout before key press operation
delay_before_keys = property(fget=delay_before_keys, fset=delay_before_keys)
def delay_between_keys(self, value=None):
"""
Same as :py:func:`GlobalConfig.toggle_delay` but with
:param value: time interval between two consecutively typed keys
"""
if value is None:
return GlobalConfig._type_delay
else:
GlobalConfig._type_delay = value
#: time interval between two consecutively typed keys
delay_between_keys = property(fget=delay_between_keys, fset=delay_between_keys)
def rescan_speed_on_find(self, value=None):
"""
Same as :py:func:`GlobalConfig.toggle_delay` but with
:param value: time interval between two image matching attempts
(used to reduce overhead on the CPU)
"""
if value is None:
return GlobalConfig._rescan_speed_on_find
else:
GlobalConfig._rescan_speed_on_find = value
#: 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(self, value=None):
"""
Getter/setter for property attribute.
:param value: whether to wait for animations to complete and match only static (not moving) targets
:type value: bool or None
:returns: current value if no argument was passed otherwise only sets it
:rtype: bool or 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 GlobalConfig._wait_for_animations
elif value is True or value is False:
GlobalConfig._wait_for_animations = value
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(self, value=None):
"""
Getter/setter for property attribute.
:param value: whether to move the mouse cursor to a location instantly or smoothly
:type value: bool or None
:returns: current value if no argument was passed otherwise only sets it
:rtype: bool or 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 GlobalConfig._smooth_mouse_drag
elif value is True or value is False:
GlobalConfig._smooth_mouse_drag = value
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(self, value=None):
"""
Same as :py:func:`GlobalConfig.smooth_mouse_drag` but with
:param value: whether to preprocess capital and special characters and
handle them internally
.. warning:: The characters will be forcefully preprocessed for the
autopy on linux (capital and special) and vncdotool (capital) backends.
"""
if value is None:
return GlobalConfig._preprocess_special_chars
elif value is True or value is False:
GlobalConfig._preprocess_special_chars = value
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(self, value=None):
"""
Same as :py:func:`GlobalConfig.smooth_mouse_drag` but with
:param value: whether to perform an extra needle dump on matching error
"""
if value is None:
return GlobalConfig._save_needle_on_error
elif value is True or value is False:
GlobalConfig._save_needle_on_error = value
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(self, value=None):
"""
Getter/setter for property attribute.
:param value: logging level similar to the python logging module
:type value: int or None
:returns: current value if no argument was passed otherwise only sets it
:rtype: int or None
.. seealso:: See the image logging documentation for more details.
"""
if value is None:
return GlobalConfig._image_logging_level
else:
GlobalConfig._image_logging_level = value
#: 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(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_level` but with
:param value: number of digits when enumerating the image
logging steps, e.g. value=3 for 001, 002, etc.
"""
if value is None:
return GlobalConfig._image_logging_step_width
else:
GlobalConfig._image_logging_step_width = value
#: 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(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_level` but with
: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)
"""
if value is None:
return GlobalConfig._image_quality
else:
GlobalConfig._image_quality = 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)
image_quality = property(fget=image_quality, fset=image_quality)
def image_logging_destination(self, value=None):
"""
Getter/setter for property attribute.
:param value: relative path of the image logging steps
:type value: str or None
:returns: current value if no argument was passed otherwise only sets it
:rtype: str or None
"""
if value is None:
return GlobalConfig._image_logging_destination
else:
GlobalConfig._image_logging_destination = value
#: relative path of the image logging steps
image_logging_destination = property(fget=image_logging_destination, fset=image_logging_destination)
def display_control_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the display control backend
:raises: :py:class:`ValueError` if value is not among the supported backends
Supported backends:
* pyautogui - Windows, Linux, and OS X compatible with both the GUI
actions and their calls executed on the same machine
* autopy - 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 GlobalConfig._display_control_backend
else:
if value not in ["autopy", "xdotool", "vncdotool", "qemu", "pyautogui"]:
raise ValueError("Unsupported backend for GUI actions '%s'" % value)
GlobalConfig._display_control_backend = value
#: 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(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the computer vision backend
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 GlobalConfig._find_backend
else:
GlobalConfig._find_backend = value
#: name of the computer vision backend
find_backend = property(fget=find_backend, fset=find_backend)
def contour_threshold_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the contour threshold backend
Supported backends: normal, adaptive, canny.
"""
if value is None:
return GlobalConfig._contour_threshold_backend
else:
GlobalConfig._contour_threshold_backend = value
#: name of the contour threshold backend
contour_threshold_backend = property(fget=contour_threshold_backend, fset=contour_threshold_backend)
def template_match_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the template matching backend
Supported backends: autopy, sqdiff, ccorr, ccoeff, sqdiff_normed,
ccorr_normed, ccoeff_normed.
"""
if value is None:
return GlobalConfig._template_match_backend
else:
GlobalConfig._template_match_backend = value
#: name of the template matching backend
template_match_backend = property(fget=template_match_backend, fset=template_match_backend)
def feature_detect_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the feature detection backend
Supported backends: BruteForce, BruteForce-L1, BruteForce-Hamming,
BruteForce-Hamming(2), in-house-raw, in-house-region.
"""
if value is None:
return GlobalConfig._feature_detect_backend
else:
GlobalConfig._feature_detect_backend = value
#: name of the feature detection backend
feature_detect_backend = property(fget=feature_detect_backend, fset=feature_detect_backend)
def feature_extract_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the feature extraction backend
Supported backends: ORB, FAST, STAR, GFTT, HARRIS, Dense, oldSURF.
"""
if value is None:
return GlobalConfig._feature_extract_backend
else:
GlobalConfig._feature_extract_backend = value
#: name of the feature extraction backend
feature_extract_backend = property(fget=feature_extract_backend, fset=feature_extract_backend)
def feature_match_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the feature matching backend
Supported backends: ORB, BRIEF, FREAK.
"""
if value is None:
return GlobalConfig._feature_match_backend
else:
GlobalConfig._feature_match_backend = value
#: name of the feature matching backend
feature_match_backend = property(fget=feature_match_backend, fset=feature_match_backend)
def text_detect_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the text detection backend
Supported backends: east, erstat, contours, components.
"""
if value is None:
return GlobalConfig._text_detect_backend
else:
GlobalConfig._text_detect_backend = value
#: name of the text detection backend
text_detect_backend = property(fget=text_detect_backend, fset=text_detect_backend)
def text_ocr_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the optical character recognition backend
Supported backends: pytesseract, tesserocr, tesseract (OpenCV), hmm, beamSearch.
"""
if value is None:
return GlobalConfig._text_ocr_backend
else:
GlobalConfig._text_ocr_backend = value
#: name of the optical character recognition backend
text_ocr_backend = property(fget=text_ocr_backend, fset=text_ocr_backend)
def deep_learn_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the deep learning backend
Supported backends: pytorch, tensorflow (partial).
"""
if value is None:
return GlobalConfig._deep_learn_backend
else:
GlobalConfig._deep_learn_backend = value
#: name of the deep learning backend
deep_learn_backend = property(fget=deep_learn_backend, fset=deep_learn_backend)
def hybrid_match_backend(self, value=None):
"""
Same as :py:func:`GlobalConfig.image_logging_destination` but with
:param value: name of the hybrid matching backend for unconfigured one-step targets
Supported backends: all nonhybrid backends of :py:func:`GlobalConfig.find_backend`.
"""
if value is None:
return GlobalConfig._hybrid_match_backend
else:
GlobalConfig._hybrid_match_backend = value
#: 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):
"""
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):
"""
Proxies a GlobalConfig instance extending it to add context
support, 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
"""
[docs] def __init__(self):
"""Build a temporary global config."""
object.__setattr__(self, "_original_values", {})
[docs] def __getattribute__(self, name):
# fallback to GlobalConfig
return getattr(GlobalConfig, name)
[docs] def __setattr__(self, name, 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)
[docs] def __enter__(self):
# our temporary config object
return self
[docs] def __exit__(self, *_):
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):
"""
Container for the configuration of all display control and
computer vision backends, responsible for making them behave
according to the selected parameters as well as for providing
information about them and the current parameters.
"""
[docs] def __init__(self, configure=True, synchronize=True):
"""
Build a container for the entire backend configuration.
:param bool configure: whether to also generate configuration
:param bool 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")
if configure:
self.__configure_backend()
if synchronize:
self.__synchronize_backend()
def __configure_backend(self, backend=None, category="type", reset=False):
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
def __synchronize_backend(self, backend=None, category="type", reset=False):
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=None, category="type", reset=False):
"""
Synchronize a category backend with the equalizer configuration.
:param backend: name of a preselected backend, see `algorithms[category]`
:type backend: str or None
:param str category: category for the backend, see `algorithms.keys()`
:param bool 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, reset=True, **kwargs):
"""
Synchronize all backends with the current configuration dictionary.
:param bool reset: whether to (re)sync all parent backends as well
"""
self.synchronize_backend(reset=reset)