Source code for dragonfly.grammar.context

#
# This file is part of Dragonfly.
# (c) Copyright 2007, 2008 by Christo Butcher
# Licensed under the LGPL.
#
#   Dragonfly 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.
#
#   Dragonfly 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 Dragonfly.  If not, see
#   <http://www.gnu.org/licenses/>.
#

"""
Context classes
============================================================================

Dragonfly uses context classes to define when grammars and
rules should be active.  A context is an object with a
:meth:`Context.matches` method which returns *True* if the
system is currently within that context, and *False* if it
is not.

The following context classes are available:

 - :class:`Context` --
   the base class from which all other context classes are derived
 - :class:`AppContext` --
   class which is based on the application context, i.e. foreground
   window executable, title, and handle
 - :class:`FuncContext` --
   class that evaluates a given function/lambda/callable, whose return
   value is interpreted as a *bool*, determining whether the context is
   active


Logical operations
----------------------------------------------------------------------------

It is possible to modify and combine the behavior of contexts using the
Python's standard logical operators:

:logical AND: ``context1 & context2`` -- *all* contexts must match
:logical OR: ``context1 | context2`` --
   *one or more* of the contexts must match
:logical NOT: ``~context1`` -- *inversion* of when the context matches

For example, to create a context which will match when
Firefox is in the foreground, but only if Google Reader is
*not* being viewed::

   firefox_context = AppContext(executable="firefox")
   reader_context = AppContext(executable="firefox", title="Google Reader")
   firefox_but_not_reader_context = firefox_context & ~reader_context


Matching other window attributes
----------------------------------------------------------------------------

The :class:`AppContext` class can be used to match window attributes and
properties other than the title and executable. To do this, pass extra
keyword arguments to the constructor::

   # Context for a maximized Firefox window.
   maximized_firefox = AppContext(executable="firefox", is_maximized=True)

   # Context for a browser in fullscreen mode.
   # 'role' and 'is_fullscreen' are X11 only.
   fullscreen_browser = AppContext(role="browser", is_fullscreen=True)

   # Context for Android Studio or PyCharm using the X11 'cls' property.
   AppContext(cls=["jetbrains-studio", "jetbrains-pycharm-ce"])



Class reference
----------------------------------------------------------------------------

"""

import copy
import inspect
import logging

from six import PY2, string_types

# --------------------------------------------------------------------------


[docs] class Context(object): """ Base class for other context classes. This base class implements some basic infrastructure, including what's required for logical operations on context objects. Derived classes should at least do the following things: - During initialization, set *self._str* to some descriptive, human readable value. This attribute is used by the ``__str__()`` method. - Overload the :meth:`Context.matches` method to implement the logic to determine when to be active. The *self._log* logger objects should be used in methods of derived classes for logging purposes. It is a standard logger object from the *logger* module in the Python standard library. """ _log = logging.getLogger("context.match") _log_match = _log # ---------------------------------------------------------------------- # Initialization and aggregation methods. def __init__(self): self._str = "" def __repr__(self): return "%s(%s)" % (self.__class__.__name__, self._str) def copy(self): return copy.deepcopy(self) # ---------------------------------------------------------------------- # Logical operations. def __and__(self, other): return LogicAndContext(self, other) def __or__(self, other): return LogicOrContext(self, other) def __invert__(self): return LogicNotContext(self) # ---------------------------------------------------------------------- # Matching methods.
[docs] def matches(self, executable, title, handle): """ Indicate whether the system is currently within this context. Arguments: - *executable* (*str*) -- path name to the executable of the foreground application - *title* (*str*) -- title of the foreground window - *handle* (*int*) -- window handle to the foreground window The default implementation of this method simply returns *True*. .. note:: This is generally the method which developers should overload to give derived context classes custom functionality. """ # pylint: disable=unused-argument,no-self-use return True
# -------------------------------------------------------------------------- # Wrapper contexts for combining contexts in logical structures.
[docs] class LogicAndContext(Context): def __init__(self, *children): Context.__init__(self) self._children = children self._str = ", ".join(str(child) for child in children)
[docs] def matches(self, executable, title, handle): for child in self._children: if not child.matches(executable, title, handle): return False return True
[docs] class LogicOrContext(Context): def __init__(self, *children): Context.__init__(self) self._children = children self._str = ", ".join(str(child) for child in children)
[docs] def matches(self, executable, title, handle): for child in self._children: if child.matches(executable, title, handle): return True return False
[docs] class LogicNotContext(Context): def __init__(self, child): Context.__init__(self) self._child = child self._str = str(child)
[docs] def matches(self, executable, title, handle): return not self._child.matches(executable, title, handle)
# --------------------------------------------------------------------------
[docs] class AppContext(Context): """ Context class using foreground application details. This class determines whether the foreground window meets certain requirements. Which requirements must be met for this context to match are determined by the constructor arguments. If multiple strings are passed in a list, True will be returned if the foreground window matches one or more of them. This applies to the *executable* and *title* arguments and key word arguments for most window attributes. Constructor arguments: - *executable* (*str* or *list*) -- (part of) the path name of the foreground application's executable; case insensitive - *title* (*str* or *list*) -- (part of) the title of the foreground window; case insensitive - *key word arguments* -- optional window attributes/properties and expected values; case insensitive """ # ---------------------------------------------------------------------- # Initialization methods. def __init__(self, executable=None, title=None, exclude=False, **kwargs): # pylint: disable=too-many-branches # Suppress warnings about too many if-else branches. Context.__init__(self) # Allow Unicode or strings to be used for executables and titles. if isinstance(executable, string_types): self._executable = [executable.lower()] elif isinstance(executable, (list, tuple)): self._executable = [e.lower() for e in executable] elif executable is None: self._executable = None else: raise TypeError("executable argument must be a string or None;" " received %r" % executable) if isinstance(title, string_types): self._title = [title.lower()] elif isinstance(title, (list, tuple)): self._title = [t.lower() for t in title] elif title is None: self._title = None else: raise TypeError("title argument must be a string or None;" " received %r" % title) # Handle keyword arguments. new_kwargs = {} for key, value in kwargs.items(): if isinstance(value, string_types): values = [value.lower()] elif isinstance(value, list): values = [str(v).lower() for v in value] elif value is None: values = None else: values = [value] new_kwargs[key] = values self._exclude = bool(exclude) self._str = "%s, %s, %s" % (self._executable, self._title, self._exclude) self._kwargs = new_kwargs if self._kwargs: self._str += ", %s" % self._kwargs # ---------------------------------------------------------------------- # Matching methods.
[docs] def matches(self, executable, title, handle): # pylint: disable=too-many-branches # Suppress warnings about too many if-else branches. if isinstance(executable, string_types): executable = executable.lower() if isinstance(title, string_types): title = title.lower() if self._executable: found = False if isinstance(executable, string_types): for match in self._executable: if executable.find(match) != -1: found = True break if self._exclude == found: self._log_match.debug("%s: No match, executable doesn't " "match.", self) return False if self._title: found = False if isinstance(title, string_types): for match in self._title: if title.find(match) != -1: found = True break if self._exclude == found: self._log_match.debug("%s: No match, title doesn't match.", self) return False if self._kwargs: # Import locally to avoid import cycles. from dragonfly.windows import Window window = Window.get_window(handle) found = False for attr, expected_values in self._kwargs.items(): try: # Get the window attribute. attr_value = getattr(window, attr) except AttributeError: self._log_match.warning("%s: Skipped missing window" " attribute '%s'", self, attr) continue # Check if the window attribute matched. for match in expected_values: is_string = isinstance(attr_value, string_types) found = ( not is_string and match == attr_value or is_string and attr_value.lower().find(match) != -1 ) if found: break if self._exclude == found: self._log_match.debug("%s: No match, not all extra" " attributes match.", self) return False if self._log_match: self._log_match.debug("%s: Match.", self) return True
# --------------------------------------------------------------------------
[docs] class FuncContext(Context): """ Context class that evaluates a given function, whose return value is interpreted as a *bool*, determining whether the context is active. The foreground application details are optionally passed to the function as arguments named *executable*, *title*, and/or *handle*, if any/each matches a so-named keyword argument of the function. Default arguments may also be passed to the function, through this class's constructor. """ def __init__(self, function, **defaults): """ Constructor arguments: - *function* (callable) -- the function to call when this context is evaluated - defaults -- optional default keyword-values for the arguments with which the function will be called """ Context.__init__(self) self._function = function self._defaults = defaults self._str = "%s, defaults: %s" % (self._function, self._defaults) getargspec = inspect.getargspec if PY2 else inspect.getfullargspec (args, _, varkw, defaults) = getargspec(self._function)[0:4] if varkw: self._filter_keywords = False else: self._filter_keywords = True self._valid_keywords = set(args)
[docs] def matches(self, executable, title, handle): arguments = dict(self._defaults) arguments.update(executable=executable, title=title, handle=handle) if self._filter_keywords: invalid_keywords = set(arguments.keys()) - self._valid_keywords for key in invalid_keywords: del arguments[key] try: match = bool(self._function(**arguments)) if match: self._log_match.debug("%s: Match.", self) else: self._log_match.debug("%s: No match, function" " returned false.", self) return match except: self._log.exception("Exception from function %s:", self._function.__name__) # Fallback to matching return True