[Checkins] SVN: z3c.quickentry/trunk/ The quick entry processor allows a user to efficiently specify multiple

Stephan Richter srichter at cosmos.phy.tufts.edu
Tue Feb 6 09:07:55 EST 2007


Log message for revision 72394:
  The quick entry processor allows a user to efficiently specify multiple 
  values in one larger text block. The processor uses plugins to 
  dynamically define the commands to handle.
  
  This type of input is not aimed at the average user, but at power users 
  and users that can be trained. The syntax is purposefully minimized to 
  maximize the input speed. This method of entry has been verified in a 
  real life setting.
  
  

Changed:
  A   z3c.quickentry/trunk/
  A   z3c.quickentry/trunk/README.txt
  A   z3c.quickentry/trunk/__init__.py
  A   z3c.quickentry/trunk/interfaces.py
  A   z3c.quickentry/trunk/plugin.py
  A   z3c.quickentry/trunk/processor.py
  A   z3c.quickentry/trunk/tests.py

-=-
Added: z3c.quickentry/trunk/README.txt
===================================================================
--- z3c.quickentry/trunk/README.txt	2007-02-06 14:04:04 UTC (rev 72393)
+++ z3c.quickentry/trunk/README.txt	2007-02-06 14:07:55 UTC (rev 72394)
@@ -0,0 +1,259 @@
+===========
+Quick Entry
+===========
+
+The quick entry processor allows a user to efficiently specify multiple values
+in one larger text block. The processor uses plugins to dynamically define the
+commands to handle.
+
+This type of input is not aimed at the average user, but at power users and
+users that can be trained. The syntax is purposefully minimized to maximize
+the input speed. This method of entry has been verified in a real life
+setting.
+
+
+Processor Plugins
+-----------------
+
+Let's start by looking at the processor plugins, which can handle one piece of
+the quick entry text. The first plugin type can handle strings of the form:
+
+  <shortName>=<value>
+
+A base implementation of this plugin is provided by the package. Let's create
+a plugin that can process a name:
+
+  >>> from z3c.quickentry import plugin
+  >>> class NamePlugin(plugin.ShortNamePlugin):
+  ...     shortName = 'nm'
+  ...     varName = 'name'
+
+Any plugin is instantiated using an initial text:
+
+  >>> name = NamePlugin('nm=Stephan')
+  >>> name
+  <NamePlugin shortName='nm', varName='name'>
+
+You can now ask the plugin, whether it can process this text:
+
+  >>> name.canProcess()
+  True
+
+  >>> NamePlugin('n=Stephan').canProcess()
+  False
+  >>> NamePlugin('Stephan').canProcess()
+  False
+
+Sometimes the processor adds more text later:
+
+  >>> name.text += ' Richter'
+
+Once all pieces have been processed by the quick entry processor, each
+instantiated plugin gets processed. The result of this action is a dictionary:
+
+  >>> name.process(None)
+  {'name': u'Stephan Richter'}
+
+The second type of plugin matches a regular expression to determine whether a
+piece of text can be processed. Let's create a phone number plugin:
+
+  >>> import re
+  >>> class PhonePlugin(plugin.RegexPlugin):
+  ...     regex = re.compile('^[0-9]{3}-[0-9]{3}-[0-9]{4}$')
+  ...     varName = 'phone'
+
+This plugin is also instantiated using an initial text:
+
+  >>> phone = PhonePlugin('978-555-5300')
+  >>> phone
+  <PhonePlugin varName='phone'>
+
+You can now ask the plugin, whether it can process this text:
+
+  >>> name.canProcess()
+  True
+
+  >>> PhonePlugin('(978) 555-5300').canProcess()
+  False
+  >>> PhonePlugin('+1-978-555-5300').canProcess()
+  False
+
+Let's now process the plugin:
+
+  >>> phone.process(None)
+  {'phone': u'978-555-5300'}
+
+If the text changes, so that the plugin cannot parse the text anymore, a value
+error is raised:
+
+  >>> phone.text += ' (ext. 2134)'
+  >>> phone.process(None)
+  Traceback (most recent call last):
+  ...
+  ValueError: The regex did match anymore. ...
+
+Finally let's have a look at a more advanced example. We would like to be able
+to handle the string "<age><gender>" and parse it into 2 variables:
+
+  >>> class AgeGenderPlugin(plugin.BasePlugin):
+  ...     regex = re.compile('([0-9]{1,3})([FM])')
+  ...
+  ...     def canProcess(self):
+  ...         return self.regex.match(self.text) is not None
+  ...
+  ...     def process(self, context):
+  ...         match = self.regex.match(self.text)
+  ...         return {'age': int(match.groups()[0]),
+  ...                 'gender': unicode(match.groups()[1])}
+
+Let's now make sure that the plugin can handle several strings:
+
+  >>> AgeGenderPlugin('27M').canProcess()
+  True
+  >>> AgeGenderPlugin('8F').canProcess()
+  True
+  >>> AgeGenderPlugin('101F').canProcess()
+  True
+  >>> AgeGenderPlugin('27N').canProcess()
+  False
+  >>> AgeGenderPlugin('M').canProcess()
+  False
+  >>> AgeGenderPlugin('18').canProcess()
+  False
+
+Let's also make sure it is processed correctly:
+
+  >>> pprint(AgeGenderPlugin('27M').process(None))
+  {'age': 27, 'gender': u'M'}
+  >>> pprint(AgeGenderPlugin('8F').process(None))
+  {'age': 8, 'gender': u'F'}
+  >>> pprint(AgeGenderPlugin('101F').process(None))
+  {'age': 101, 'gender': u'F'}
+
+The plugin above used the ``BasePlugin`` class to minimize the
+boilerplate. The base plugin requires you to implement the ``canProcess()``
+and ``process()``:
+
+  >>> base = plugin.BasePlugin('some text')
+
+  >>> base.canProcess()
+  Traceback (most recent call last):
+  ...
+  NotImplementedError
+
+  >>> base.process(None)
+  Traceback (most recent call last):
+  ...
+  NotImplementedError
+
+
+Executing Plugins
+-----------------
+
+An optional feature of the package is the ability for the plugin to apply the
+parsed data directly to a specified context. The simplest such case is to
+simply set the attribute on the context. For this use case we have a mix-in
+class:
+
+  >>> class ExecutingAgeGenderPlugin(AgeGenderPlugin, plugin.SetAttributeMixin):
+  ...     pass
+
+Let's now create a person on which the attributes can be stored:
+
+  >>> class Person(object):
+  ...     name = None
+  ...     phone = None
+  ...     age = None
+  ...     gender = None
+  >>> stephan = Person()
+
+Let's now apply the executing age/gender plugin onto the person:
+
+  >>> stephan.age
+  >>> stephan.gender
+
+  >>> ExecutingAgeGenderPlugin('27M').apply(stephan)
+
+  >>> stephan.age
+  27
+  >>> stephan.gender
+  u'M'
+
+
+Processors
+----------
+
+The processor collects several plugins and handles one large chunk of quick
+entry text. Let's create a processor for the plugins above, using the default
+whitespace character as field separator:
+
+  >>> from z3c.quickentry import processor
+  >>> info = processor.BaseProcessor()
+  >>> info.plugins = (NamePlugin, PhonePlugin, AgeGenderPlugin)
+
+The lowest level step of the processor is the parsing of the text; the result
+is a sequence of plugin instances:
+
+  >>> info.parse('nm=Stephan 27M')
+  [<NamePlugin shortName='nm', varName='name'>, <AgeGenderPlugin '27M'>]
+
+Let's now parse and process a simple texts that uses some or all plugins:
+
+  >>> pprint(info.process('nm=Stephan 27M'))
+  {'age': 27, 'gender': u'M', 'name': u'Stephan'}
+
+  >>> pprint(info.process('978-555-5300 27M'))
+  {'age': 27, 'gender': u'M', 'phone': u'978-555-5300'}
+
+  >>> pprint(info.process('nm=Stephan 978-555-5300 27M'))
+  {'age': 27, 'gender': u'M', 'name': u'Stephan', 'phone': u'978-555-5300'}
+
+Note that you can also have names that contain spaces, because the last name
+cannot be matched to another plugin:
+
+  >>> pprint(info.process('nm=Stephan Richter 27M'))
+  {'age': 27, 'gender': u'M', 'name': u'Stephan Richter'}
+
+Optionally, you can also provide a processing context that can be used to look
+up values (for example vocabularies):
+
+  >>> pprint(info.process('nm=Stephan Richter 27M', context=object()))
+  {'age': 27, 'gender': u'M', 'name': u'Stephan Richter'}
+
+Let's now change the separation character to a comma:
+
+  >>> info.separationCharacter = ','
+  >>> pprint(info.process('nm=Stephan Richter,27M', context=object()))
+  {'age': 27, 'gender': u'M', 'name': u'Stephan Richter'}
+
+
+Executing Processors
+--------------------
+
+These processors can apply all of the plugins on a context. Let's convert the
+remaining plugins to be executable:
+
+  >>> class ExecutingNamePlugin(NamePlugin, plugin.SetAttributeMixin):
+  ...     pass
+
+  >>> class ExecutingPhonePlugin(PhonePlugin, plugin.SetAttributeMixin):
+  ...     pass
+
+Let's now create a new user and create an executing processor:
+
+  >>> stephan = Person()
+
+  >>> proc = processor.ExecutingBaseProcessor()
+  >>> proc.plugins = (
+  ...     ExecutingNamePlugin, ExecutingPhonePlugin, ExecutingAgeGenderPlugin)
+
+  >>> proc.apply('nm=Stephan 978-555-5300 27M', stephan)
+
+  >>> stephan.name
+  u'Stephan'
+  >>> stephan.phone
+  u'978-555-5300'
+  >>> stephan.age
+  27
+  >>> stephan.gender
+  u'M'


Property changes on: z3c.quickentry/trunk/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: z3c.quickentry/trunk/__init__.py
===================================================================
--- z3c.quickentry/trunk/__init__.py	2007-02-06 14:04:04 UTC (rev 72393)
+++ z3c.quickentry/trunk/__init__.py	2007-02-06 14:07:55 UTC (rev 72394)
@@ -0,0 +1 @@
+# Make a package


Property changes on: z3c.quickentry/trunk/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.quickentry/trunk/interfaces.py
===================================================================
--- z3c.quickentry/trunk/interfaces.py	2007-02-06 14:04:04 UTC (rev 72393)
+++ z3c.quickentry/trunk/interfaces.py	2007-02-06 14:07:55 UTC (rev 72394)
@@ -0,0 +1,81 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Quick Entry Interfaces
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.interface
+
+class IProcessor(zope.interface.Interface):
+    """A processor for a quick entry text."""
+
+    separationCharacter = zope.interface.Attribute(
+        'Each value is separated by this character.')
+
+    plugins = zope.interface.Attribute(
+        'A sequence of plugin classes that are used to parse the text.')
+
+    def parse(text):
+        """Parse the text into a tuple of plugin instances."""
+
+    def process(text, context=None):
+        """Process a quick entry text.
+
+        The context can be used by plugins to look up values.
+
+        The returned value should be a dictionary of extracted variables.
+        """
+
+class IExecutingProcessor(IProcessor):
+    """A processor that can apply the parsed data to a context."""
+
+    def apply(text, context):
+        """Apply data once it is parsed.
+
+        The data is applied on the context.
+        """
+
+
+class IPlugin(zope.interface.Interface):
+    """A plugin for a particular piece of the quick entry text."""
+
+    text = zope.interface.Attribute(
+        'The text that is going to be converted into values. '
+        'The processor will fill this attribute after the initial text is set.')
+
+    def canProcess():
+        """Determine whether the plugin can handle the text.
+
+        Returns a boolean stating the result.
+        """
+
+    def process(context):
+        """Process the text to create the varaiable names.
+
+        The result will be a dictionary from variable name to value. While
+        plugins often will only produce one variable, they can produce several.
+
+        If processing fails for some reason, a ``ValueError`` with a detailed
+        explanation must be raised.
+        """
+
+class IExecutingPlugin(IPlugin):
+    """A plugin that can apply the parsed data to a context."""
+
+    def apply(text, context=None):
+        """Apply data once it is parsed.
+
+        The data is applied on the context.
+        """


Property changes on: z3c.quickentry/trunk/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.quickentry/trunk/plugin.py
===================================================================
--- z3c.quickentry/trunk/plugin.py	2007-02-06 14:04:04 UTC (rev 72393)
+++ z3c.quickentry/trunk/plugin.py	2007-02-06 14:07:55 UTC (rev 72394)
@@ -0,0 +1,89 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Quick Entry Processor Plugin Implementation
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import re
+import zope.interface
+from z3c.quickentry import interfaces
+
+class BasePlugin(object):
+    """An abstract base plugin."""
+    zope.interface.implements(interfaces.IPlugin)
+
+    def __init__(self, initialText):
+        self.text = initialText
+
+    def canProcess(self):
+        """See interfaces.IPlugin"""
+        raise NotImplementedError
+
+    def process(self, context):
+        """See interfaces.IPlugin"""
+        raise NotImplementedError
+
+    def __repr__(self):
+        return '<%s %r>' % (self.__class__.__name__, self.text)
+
+
+class ShortNamePlugin(BasePlugin):
+    """Abstract plugin that retrieves a value by short name assignment."""
+    # This needs to be overridden by a subclass.
+    shortName = 'sn'
+    varName = 'shortName'
+
+    def canProcess(self):
+        """See interfaces.IPlugin"""
+        return self.text.startswith(self.shortName + '=')
+
+    def process(self, context):
+        """See interfaces.IPlugin"""
+        return {self.varName: unicode(self.text[len(self.shortName)+1:])}
+
+    def __repr__(self):
+        return '<%s shortName=%r, varName=%r>' % (
+            self.__class__.__name__, self.shortName, self.varName)
+
+
+class RegexPlugin(BasePlugin):
+    """Abstract Plugin that determines the ability to process using a regular
+    expression.
+    """
+    # This needs to be overridden by a subclass.
+    regex = None
+    varName = ''
+
+    def canProcess(self):
+        """See interfaces.IPlugin"""
+        return self.regex.match(self.text) is not None
+
+    def process(self, context):
+        """See interfaces.IPlugin"""
+        if self.regex.match(self.text) is None:
+            raise ValueError('The regex did match anymore. Probably some text '
+                             'was added later that disrupted the pattern.')
+        return {self.varName: unicode(self.text)}
+
+    def __repr__(self):
+        return '<%s varName=%r>' % (self.__class__.__name__, self.varName)
+
+
+class SetAttributeMixin(object):
+    zope.interface.implements(interfaces.IExecutingPlugin)
+
+    def apply(self, context):
+        for name, value in self.process(context).items():
+            setattr(context, name, value)


Property changes on: z3c.quickentry/trunk/plugin.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.quickentry/trunk/processor.py
===================================================================
--- z3c.quickentry/trunk/processor.py	2007-02-06 14:04:04 UTC (rev 72393)
+++ z3c.quickentry/trunk/processor.py	2007-02-06 14:07:55 UTC (rev 72394)
@@ -0,0 +1,69 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Quick Entry Processor Implementation
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.interface
+from z3c.quickentry import interfaces
+
+class BaseProcessor(object):
+    """A base class for a processor."""
+    zope.interface.implements(interfaces.IProcessor)
+
+    # See interfaces.IProcessor
+    separationCharacter = ' '
+    # This needs to be properly implemented in a subclass.
+    plugins = ()
+
+    def parse(self, text):
+        # Step 0: Get the sequence of all plugins; we store the result
+        #         locally, since the lookup might be expensive.
+        plugins = self.plugins
+        # Step 1: Split the entire text using the separation character
+        pieces = text.split(self.separationCharacter)
+        # Whenever a plugin is found that can handle a piece of text, it is
+        # added to the result list
+        result = []
+        # Step 2: Now iterate through every piece and try to process them
+        for piece in pieces:
+            # Step 2.1: Check each plugin to see if it can process the piece
+            for pluginClass in plugins:
+                plugin = pluginClass(piece)
+                # Step 2.2: If the plugin can process, add it to the result
+                if plugin.canProcess():
+                    result.append(plugin)
+                    break
+            # Step 2.3: If no plugin can handle the piece, it is simply added
+            #           to the text of the last plugin's test.
+            else:
+                result[-1].text += self.separationCharacter
+                result[-1].text += piece
+        return result
+
+    def process(self, text, context=None):
+        """See interfaces.IProcessor"""
+        resultDict = {}
+        for plugin in self.parse(text):
+            resultDict.update(plugin.process(context))
+        return resultDict
+
+
+class ExecutingBaseProcessor(BaseProcessor):
+
+    def apply(self, text, context):
+        """See interfaces.IProcessor"""
+        for plugin in self.parse(text):
+            plugin.apply(context)


Property changes on: z3c.quickentry/trunk/processor.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.quickentry/trunk/tests.py
===================================================================
--- z3c.quickentry/trunk/tests.py	2007-02-06 14:04:04 UTC (rev 72393)
+++ z3c.quickentry/trunk/tests.py	2007-02-06 14:07:55 UTC (rev 72394)
@@ -0,0 +1,36 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Quick Entry Tests
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+import unittest
+from zope.testing import doctest
+from zope.testing import doctestunit
+from zope.app.testing import placelesssetup
+
+def test_suite():
+    return unittest.TestSuite((
+        doctestunit.DocFileSuite(
+            'README.txt',
+            globs={'pprint': doctestunit.pprint},
+            setUp=placelesssetup.setUp, tearDown=placelesssetup.tearDown,
+            optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+            ),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')


Property changes on: z3c.quickentry/trunk/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id



More information about the Checkins mailing list