Source code for dragonfly.grammar.elements_compound

#
# 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/>.
#

"""
Compound element classes
============================================================================

The following special *element* classes exist as convenient ways of
constructing basic element types from string specifications:

 * :class:`Compound` --
   a special element which parses a string spec to create a hierarchy of
   basic elements.

 * :class:`Choice` --
   a special element taking a :code:`choice` dictionary argument,
   interpreting keys as :class:`Compound` string specifications and values
   for what to return when compound specs are successfully decoded during
   the recognition process.

   The :code:`choice` argument may also be a list or tuple of strings, in
   which case the strings are also interpreted as :class:`Compound` strings
   specifications.  However, the values returned when compound specs are
   successfully decoded during the recognition process are the recognized
   words.  **Note**: these values will be matching part(s) of the compound
   specs.

"""

# pylint: disable=W0223
# Suppress abstract method warnings.

import locale
import logging
import re

from six                               import string_types, binary_type

from dragonfly.grammar.elements_basic  import Alternative, ElementBase
from dragonfly.parsing.parse           import (spec_parser, ParseError,
                                              CompoundTransformer)

#---------------------------------------------------------------------------
# The Compound class.

[docs] class Compound(Alternative): """ Element which parses a string spec to create a hierarchy of basic elements. Constructor arguments: - *spec* (*str*) -- compound specification - *extras* (sequence) -- extras elements referenced from the compound spec - *actions* (*dict*) -- this argument is currently unused - *name* (*str*) -- the name of this element - *value* (*object*, default: *None*) -- value to be returned when this element is successfully decoded If *None*, then the value(s) of child nodes are used instead - *value_func* (*callable*, default: *None*) -- function to be called for the node value when this element is successfully decoded. If *None*, then the value(s) of child nodes are used. This argument takes precedence over the *value* argument if it is present - *elements* (sequence) -- same as the extras argument - *default* (default: *None*) -- the default value of this element Example: .. code:: python # Define a command to type the sum of two spoken integers between # 1 and 50 using a Compound element. mapping = { "type <XAndY>": Text("%(XAndY)d"), } extras = [ Compound( # Pass the compound spec and element name. spec="<x> and <y>", name="XAndY", # Pass the internal IntegerRef extras. extras=[IntegerRef("x", 1, 50), IntegerRef("y", 1, 50)], # Pass a value function that adds the two spoken integers # together. value_func=lambda node, extras: extras["x"] + extras["y"]) ] """ _log = logging.getLogger("compound.parse") _parser = spec_parser def __init__(self, spec, extras=None, actions=None, name=None, value=None, value_func=None, elements=None, default=None): # pylint: disable=too-many-arguments,too-many-branches if isinstance(spec, binary_type): spec = spec.decode(locale.getpreferredencoding()) self._spec = spec self._value = value self._value_func = value_func if extras is None: extras = {} if actions is None: actions = {} if elements is None: elements = {} # Convert extras argument from sequence to mapping. if isinstance(extras, (tuple, list)): mapping = {} for element in extras: if not isinstance(element, ElementBase): self._log.error("Invalid extras item: %s", element) raise TypeError("Invalid extras item: %s" % element) if not element.name: self._log.error("Extras item does not have a name: %s", element) raise TypeError("Extras item does not have a name: %s" % element) if element.name in mapping: self._log.warning("Multiple extras items with the same name: %s", element) mapping[element.name] = element extras = mapping elif not isinstance(extras, dict): self._log.error("Invalid extras argument: %s", extras) raise TypeError("Invalid extras argument: %s" % extras) # Temporary transition code so that both "elements" and "extras" # are supported as keyword arguments. if extras and elements: extras = dict(extras) extras.update(elements) elif elements: extras = elements self._extras = extras try: tree = self._parser.parse(spec) except Exception as e: self._log.error("Exception raised parsing %r: %s", spec, e) raise ParseError("Exception raised parsing %r: %s" % (spec, e)) try: element = CompoundTransformer(self._extras).transform(tree) except Exception as e: self._log.error("Exception raised transforming %r: %s", spec, e) raise ParseError("Exception raised transforming %r: %s" % (spec, e)) Alternative.__init__(self, (element,), name=name, default=default) def __repr__(self): arguments = ["%r" % self._spec] if self.name: arguments.append("name=%r" % self.name) arguments = ", ".join(arguments) return "%s(%s)" % (self.__class__.__name__, arguments)
[docs] def value(self, node): if self._value_func is not None: # Prepare *extras* dict for passing to value_func(). extras = {"_node": node} for name, element in self._extras.items(): extra_node = node.get_child_by_name(name, shallow=True) if extra_node: extras[name] = extra_node.value() elif element.has_default(): extras[name] = element.default try: value = self._value_func(node, extras) except Exception as e: self._log.warning("Exception from value_func: %s", e) raise return value elif self._value is not None: return self._value else: return Alternative.value(self, node)
#--------------------------------------------------------------------------- # The Choice class which maps multiple Compound instances to values.
[docs] class Choice(Alternative): """ Element allowing a dictionary of phrases (compound specs) to be recognized to be mapped to objects to be used in an action. A list or tuple of phrases to be recognized may also be used. In this case the strings are also interpreted as :class:`Compound` string specifications. However, the values returned when compound specs are successfully decoded during the recognition process are the recognized words. **Note**: these values will be matching part(s) of the compound specs. Constructor arguments: - *name* (*str*) -- the name of this element - *choices* (*dict*, *list* or *tuple*) -- dictionary mapping recognized phrases to returned values **or** a list/tuple of recognized phrases - *extras* (*list*, default: *None*) -- a list of included extras - *default* (default: *None*) -- the default value of this element Example using a dictionary: .. code:: python # Tab switching command, e.g. 'third tab'. mapping = { "<nth> tab": Key("c-%(nth)s"), } extras = [ Choice("nth", { "first" : "1", "second" : "2", "third" : "3", "fourth" : "4", "fifth" : "5", "sixth" : "6", "seventh" : "7", "eighth" : "8", "(last | ninth)": "9", "next" : "pgdown", "previous" : "pgup", }), ] Example using a list: .. code:: python # Command for recognizing and typing nth words, e.g. # 'type third'. mapping = { "type <nth>": Text("%(nth)s"), } extras = [ Choice("nth", [ "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", # Note that the decoded value for a compound spec like # this one, when used in a list/tuple of choices, # rather than a dictionary, is the recognized word: # "last" or "ninth". "(last | ninth)", "next", "previous", ]), ] """ def __init__(self, name, choices, extras=None, default=None): # Argument type checking. assert isinstance(name, string_types) or name is None assert isinstance(choices, (dict, list, tuple)) choices_is_sequence = isinstance(choices, (list, tuple)) if choices_is_sequence: choices = {k: None for k in choices} for k, v in choices.items(): assert isinstance(k, string_types) # Construct children from the given choice keys and values. self._choices = choices self._extras = extras children = [] for k, v in choices.items(): child = Compound(spec=k, value=v, extras=extras) children.append(child) # Initialize super class. Alternative.__init__(self, children=children, name=name, default=default)