Skip to content

Commit

Permalink
Complete rewrite to allow for branching, with a more exciting specifi…
Browse files Browse the repository at this point in the history
…cation format.
  • Loading branch information
barometz committed Sep 2, 2012
1 parent 7ce5e2b commit 9192d49
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 151 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
*~

*.py[co]

# Packages
Expand Down
35 changes: 35 additions & 0 deletions demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
#
# Demonstration of Interro
#
# Copyright (c) 2012 Dominic van Berkel. See LICENSE for details.
#

import interro2 as inter

c = inter.Interro(start='TOS')

c.add(inter.YesNoQ('TOS',
question='Do you agree to the TOS?',
onanswer={True: 'email'},
default='noTOS'))
c.add(inter.StringQ('email',
question='What is your email address?',
message='We will not share this with bad people',
validation=[(lambda x: '@' in x, 'Invalid address.'),
(lambda x: x != None, 'Cannot be empty')],
confirm=True))
c.add(inter.InterroQ('noTOS',
message='Well, that\'s unfortunate. Bye!'))

c.start()

while c.messages:
print(c.messages.pop(0))
while not c.complete:
response = input('> ')
c.answer(response)
while c.messages:
print(c.messages.pop(0))

print(c.results())
55 changes: 0 additions & 55 deletions interro-demo.py

This file was deleted.

227 changes: 131 additions & 96 deletions interro.py
Original file line number Diff line number Diff line change
@@ -1,131 +1,166 @@
#!/usr/bin/env python3
# A module to interactively extract information from users.
#
# Copyright (c) 2012 Dominic van Berkel. See LICENSE for details.
#

class Datum:
"""Stores the value and validation checks for one piece of information.
validation is a list of (test, error) tuples. In this list, test must be
a function taking one argument, evaluating to True or False depending on
whether the argument is considered valid.
"""
name = None

class InterroQ:
name = ''
question = ''
message = ''
default = None
onanswer = None
validation = None
confirm = False
type_validation = None
error = None
value = None
question = None
message = None
confirm = False
validation = None

def __init__(self, name, question, confirm=False, validation=[], message=None):
"""Create a new Datum.
name: the name of this Datum
question: the question to ask the user.
confirm: Whether the system should ask for confirmation before
committing this Datum.
"""
def __init__(self, name, question='', message='', default=None,
onanswer=None, validation=None, confirm=False):
self.name = name
self.question = question
self.message = message
self.default = default
self.onanswer = onanswer or {}
self.validation = validation or []
self.confirm = confirm
self.validation = []
for test, error in validation:
self.validation.append((test, error))
self.add_typechecks()

def add_typechecks(self, *args):
self.type_validation = args

def validate(self):
"""Check the current value for compliance with all validation tests.
def preprocess(self, value):
return value

Returns false immediately when any test fails, otherwise True.
"""
def validate(self, value):
value = self.preprocess(value)
for test, error in self.type_validation:
if not test(value):
self.error = error
return False
for test, error in self.validation:
if not test(self.value):
if not test(value):
self.error = error
return False
self.error = None
return True

def setandcheck(self, value):
"""Set a new value, and return True if the value is valid."""
self.value = value
return self.validate()

def parse(self, value):
return self.preprocess(value)

class Interro:
"""The "interrogation" class.
Keeps track of the desired Datum objects and keeps asking until it's
satisfied that all of them validate.
The interface to the outside is rather procedural, mostly in an attempt to
support asynchronous users such as IRC bots that can't just stick around
in a for loop until the conversation's done.
"""
data = None
_todo = None
_current = None
_pendingconfirmation = False
def store(self, value):
self.value = self.parse(value)

def __init__(self):
self.data = []
self._todo = []
def nextq(self):
if self.onanswer and self.value in self.onanswer:
return self.onanswer[self.value]
else:
return self.default


def adddatum(self, name, question, confirm=False, validation=[], message=None):
"""Convenience wrapper for Datum.__init__()"""
self.data.append(Datum(name, question, confirm, validation, message))
class StringQ(InterroQ):
empty_allowed = False

def __init__(self, name, empty_allowed=False, **kwargs):
self.empty_allowed = empty_allowed
super().__init__(name, **kwargs)

def add_typechecks(self, *args):
empty = (self.test_empty, 'This may not be empty.')
super().add_typechecks(empty, *args)

def preprocess(self, value):
return value.strip()

def test_empty(self, value):
if self.empty_allowed:
return True
else:
if value not in [None, '']:
return True
else:
return False

def start(self):
"""Fresh start, creating a fresh queue of unasked questions."""
self._todo = list(self.data)
self.nextdatum()

class YesNoQ(InterroQ):
def add_typechecks(self, *args):
yesno = (lambda x: x in ['y', 'yes', 'n', 'no'], 'Please enter yes or no.')
super().add_typechecks(yesno, *args)

def preprocess(self, value):
return value.strip().lower()

def parse(self, value):
value = self.preprocess(value)
if value in ['y', 'yes']:
return True
else:
return False


class Interro:
tree = None
current = None
messages = None
complete = False
_start = ''
_pendinganswer = None
_pendingconfirmation = False

def __init__(self, start='start'):
self.messages = []
self.tree = {}
self._start = start

def results(self):
"""Returns all results that have been collected so far"""
res = {}
for item in self.data:
if (item.value != None):
res[item.name] = item.value
for q in self.tree.values():
if q.value is not None:
res[q.name] = q.value
return res

def question(self):
"""Returns the current question or None when all is done."""
if self._current:
message = ''
if self._pendingconfirmation:
message = 'You entered {d.value}. Are you sure? [yes/no]'
else:
if self._current.error:
message = 'Error: {d.error}\n'
message += '{d.question}'
if self._current.value is None and self._current.message:
message += '\n{d.message}'
return message.format(d=self._current)
else:
return None

def answer(self, value):
"""Provide an answer for the last question asked.
def add(self, datum):
self.tree[datum.name] = datum

def start(self):
self._nextdatum(goto=self._start)

Returns nothing, but may advance the Interro object to the next datum
when the answer is valid and/or confirmed."""
def answer(self, value):
cur = self.current
if self._pendingconfirmation:
value = value.lower()
value = value.strip().lower()
if value in ['yes', 'y']:
self._pendingconfirmation = False
self.nextdatum()
elif value in ['no', 'n']:
self._pendingconfirmation = False
self._nextdatum()
else:
self.messages.append(cur.question)
self._pendingconfirmation = False
else:
if self._current.setandcheck(value):
if self._current.confirm:
if cur.validate(value):
cur.store(value)
if cur.confirm:
self._pendingconfirmation = True
confirmq = 'You entered {value}. Are you certain? [yes/no]'
self.messages.append(confirmq.format(value=value))
else:
self.nextdatum()

def nextdatum(self):
"""Go to the next required Datum, or set it to None when done."""
if len(self._todo):
self._current = self._todo.pop(0)
self._nextdatum()
else:
self.messages.append('Error: {0}'.format(cur.error))
self.messages.append(cur.question)

def _nextdatum(self, goto=None):
if self.current:
nextq = self.current.nextq()
else:
self._current = None
nextq = None
if goto is None and nextq is None:
self.complete = True
else:
goto = goto or nextq
self.current = self.tree[goto]
if self.current.question:
self.messages.append(self.current.question)
if self.current.message:
self.messages.append(self.current.message)
else:
self.complete = True

0 comments on commit 9192d49

Please sign in to comment.