Code Coverage for nltk.draw.srparser
Untested Functions
- demo()
- ShiftReduceDemo: __init__(), _animate_reduce(), _animate_reduce_frame(), _animate_shift(), _animate_shift_frame(), _clear_hover(), _draw_stack_top(), _highlight_hover(), _highlight_productions(), _init_bindings(), _init_buttons(), _init_canvas(), _init_feedback(), _init_fonts(), _init_grammar(), _init_menubar(), _popup_reduce(), _prodlist_select(), _redraw(), _toggle_grammar(), about(), destroy(), edit_grammar(), edit_sentence(), help(), mainloop(), postscript(), reduce(), reset(), resize(), set_grammar(), set_sentence(), shift(), step(), undo()
"""
A graphical tool for exploring the shift/reduce parser.
The shift/reduce parser maintains a stack, which records the structure
of the portion of the text that has been parsed. The stack is
initially empty. Its contents are shown on the left side of the main
canvas.
On the right side of the main canvas is the remaining text. This is
the portion of the text which has not yet been considered by the
parser.
The parser builds up a tree structure for the text using two
operations:
- "shift" moves the first token from the remaining text to the top
of the stack. In the demo, the top of the stack is its right-hand
side.
- "reduce" uses a grammar production to combine the rightmost stack
elements into a single tree token.
You can control the parser's operation by using the "shift" and
"reduce" buttons; or you can use the "step" button to let the parser
automatically decide which operation to apply. The parser uses the
following rules to decide which operation to apply:
- Only shift if no reductions are available.
- If multiple reductions are available, then apply the reduction
whose CFG production is listed earliest in the grammar.
The "reduce" button applies the reduction whose CFG production is
listed earliest in the grammar. There are two ways to manually choose
which reduction to apply:
- Click on a CFG production from the list of available reductions,
on the left side of the main window. The reduction based on that
production will be applied to the top of the stack.
- Click on one of the stack elements. A popup window will appear,
containing all available reductions. Select one, and it will be
applied to the top of the stack.
Note that reductions can only be applied to the top of the stack.
Keyboard Shortcuts::
[Space]\t Perform the next shift or reduce operation
[s]\t Perform a shift operation
[r]\t Perform a reduction operation
[Ctrl-z]\t Undo most recent operation
[Delete]\t Reset the parser
[g]\t Show/hide available production list
[Ctrl-a]\t Toggle animations
[h]\t Help
[Ctrl-p]\t Print
[q]\t Quit
"""
"""
Possible future improvements:
- button/window to change and/or select text. Just pop up a window
with an entry, and let them modify the text; and then retokenize
it? Maybe give a warning if it contains tokens whose types are
not in the grammar.
- button/window to change and/or select grammar. Select from
several alternative grammars? Or actually change the grammar? If
the later, then I'd want to define nltk.draw.cfg, which would be
responsible for that.
"""
import string
from Tkinter import *
import tkFont
from nltk import parse, tokenize
from nltk.draw.tree import *
from nltk.draw import *
from nltk.draw.cfg import CFGEditor
class ShiftReduceDemo(object):
"""
A graphical tool for exploring the shift/reduce parser. The tool
displays the parser's stack and the remaining text, and allows the
user to control the parser's operation. In particular, the user
can shift tokens onto the stack, and can perform reductions on the
top elements of the stack. A "step" button simply steps through
the parsing process, performing the operations that
C{parse.ShiftReduceParser} would use.
"""
def __init__(self, grammar, sent, trace=0):
self._sent = sent
self._parser = parse.SteppingShiftReduceParser(grammar, trace)
self._top = Tk()
self._top.title('Shift Reduce Parser Demo')
self._animating_lock = 0
self._animate = IntVar(self._top)
self._animate.set(10)
self._show_grammar = IntVar(self._top)
self._show_grammar.set(1)
self._init_fonts(self._top)
self._init_bindings()
self._init_menubar(self._top)
self._init_buttons(self._top)
self._init_feedback(self._top)
self._init_grammar(self._top)
self._init_canvas(self._top)
self._reduce_menu = Menu(self._canvas, tearoff=0)
self.reset()
self._lastoper1['text'] = ''
def _init_fonts(self, root):
self._sysfont = tkFont.Font(font=Button()["font"])
root.option_add("*Font", self._sysfont)
self._size = IntVar(root)
self._size.set(self._sysfont.cget('size'))
self._boldfont = tkFont.Font(family='helvetica', weight='bold',
size=self._size.get())
self._font = tkFont.Font(family='helvetica',
size=self._size.get())
def _init_grammar(self, parent):
self._prodframe = listframe = Frame(parent)
self._prodframe.pack(fill='both', side='left', padx=2)
self._prodlist_label = Label(self._prodframe,
font=self._boldfont,
text='Available Reductions')
self._prodlist_label.pack()
self._prodlist = Listbox(self._prodframe, selectmode='single',
relief='groove', background='white',
foreground='#909090',
font=self._font,
selectforeground='#004040',
selectbackground='#c0f0c0')
self._prodlist.pack(side='right', fill='both', expand=1)
self._productions = list(self._parser.grammar().productions())
for production in self._productions:
self._prodlist.insert('end', (' %s' % production))
self._prodlist.config(height=min(len(self._productions), 25))
if 1:
listscroll = Scrollbar(self._prodframe,
orient='vertical')
self._prodlist.config(yscrollcommand = listscroll.set)
listscroll.config(command=self._prodlist.yview)
listscroll.pack(side='left', fill='y')
self._prodlist.bind('<<ListboxSelect>>', self._prodlist_select)
self._hover = -1
self._prodlist.bind('<Motion>', self._highlight_hover)
self._prodlist.bind('<Leave>', self._clear_hover)
def _init_bindings(self):
self._top.bind('<Control-q>', self.destroy)
self._top.bind('<Control-x>', self.destroy)
self._top.bind('<Alt-q>', self.destroy)
self._top.bind('<Alt-x>', self.destroy)
self._top.bind('<space>', self.step)
self._top.bind('<s>', self.shift)
self._top.bind('<Alt-s>', self.shift)
self._top.bind('<Control-s>', self.shift)
self._top.bind('<r>', self.reduce)
self._top.bind('<Alt-r>', self.reduce)
self._top.bind('<Control-r>', self.reduce)
self._top.bind('<Delete>', self.reset)
self._top.bind('<u>', self.undo)
self._top.bind('<Alt-u>', self.undo)
self._top.bind('<Control-u>', self.undo)
self._top.bind('<Control-z>', self.undo)
self._top.bind('<BackSpace>', self.undo)
self._top.bind('<Control-p>', self.postscript)
self._top.bind('<Control-h>', self.help)
self._top.bind('<F1>', self.help)
self._top.bind('<Control-g>', self.edit_grammar)
self._top.bind('<Control-t>', self.edit_sentence)
self._top.bind('-', lambda e,a=self._animate:a.set(20))
self._top.bind('=', lambda e,a=self._animate:a.set(10))
self._top.bind('+', lambda e,a=self._animate:a.set(4))
def _init_buttons(self, parent):
self._buttonframe = buttonframe = Frame(parent)
buttonframe.pack(fill='none', side='bottom')
Button(buttonframe, text='Step',
background='#90c0d0', foreground='black',
command=self.step,).pack(side='left')
Button(buttonframe, text='Shift', underline=0,
background='#90f090', foreground='black',
command=self.shift).pack(side='left')
Button(buttonframe, text='Reduce', underline=0,
background='#90f090', foreground='black',
command=self.reduce).pack(side='left')
Button(buttonframe, text='Undo', underline=0,
background='#f0a0a0', foreground='black',
command=self.undo).pack(side='left')
def _init_menubar(self, parent):
menubar = Menu(parent)
filemenu = Menu(menubar, tearoff=0)
filemenu.add_command(label='Reset Parser', underline=0,
command=self.reset, accelerator='Del')
filemenu.add_command(label='Print to Postscript', underline=0,
command=self.postscript, accelerator='Ctrl-p')
filemenu.add_command(label='Exit', underline=1,
command=self.destroy, accelerator='Ctrl-x')
menubar.add_cascade(label='File', underline=0, menu=filemenu)
editmenu = Menu(menubar, tearoff=0)
editmenu.add_command(label='Edit Grammar', underline=5,
command=self.edit_grammar,
accelerator='Ctrl-g')
editmenu.add_command(label='Edit Text', underline=5,
command=self.edit_sentence,
accelerator='Ctrl-t')
menubar.add_cascade(label='Edit', underline=0, menu=editmenu)
rulemenu = Menu(menubar, tearoff=0)
rulemenu.add_command(label='Step', underline=1,
command=self.step, accelerator='Space')
rulemenu.add_separator()
rulemenu.add_command(label='Shift', underline=0,
command=self.shift, accelerator='Ctrl-s')
rulemenu.add_command(label='Reduce', underline=0,
command=self.reduce, accelerator='Ctrl-r')
rulemenu.add_separator()
rulemenu.add_command(label='Undo', underline=0,
command=self.undo, accelerator='Ctrl-u')
menubar.add_cascade(label='Apply', underline=0, menu=rulemenu)
viewmenu = Menu(menubar, tearoff=0)
viewmenu.add_checkbutton(label="Show Grammar", underline=0,
variable=self._show_grammar,
command=self._toggle_grammar)
viewmenu.add_separator()
viewmenu.add_radiobutton(label='Tiny', variable=self._size,
underline=0, value=10, command=self.resize)
viewmenu.add_radiobutton(label='Small', variable=self._size,
underline=0, value=12, command=self.resize)
viewmenu.add_radiobutton(label='Medium', variable=self._size,
underline=0, value=14, command=self.resize)
viewmenu.add_radiobutton(label='Large', variable=self._size,
underline=0, value=18, command=self.resize)
viewmenu.add_radiobutton(label='Huge', variable=self._size,
underline=0, value=24, command=self.resize)
menubar.add_cascade(label='View', underline=0, menu=viewmenu)
animatemenu = Menu(menubar, tearoff=0)
animatemenu.add_radiobutton(label="No Animation", underline=0,
variable=self._animate, value=0)
animatemenu.add_radiobutton(label="Slow Animation", underline=0,
variable=self._animate, value=20,
accelerator='-')
animatemenu.add_radiobutton(label="Normal Animation", underline=0,
variable=self._animate, value=10,
accelerator='=')
animatemenu.add_radiobutton(label="Fast Animation", underline=0,
variable=self._animate, value=4,
accelerator='+')
menubar.add_cascade(label="Animate", underline=1, menu=animatemenu)
helpmenu = Menu(menubar, tearoff=0)
helpmenu.add_command(label='About', underline=0,
command=self.about)
helpmenu.add_command(label='Instructions', underline=0,
command=self.help, accelerator='F1')
menubar.add_cascade(label='Help', underline=0, menu=helpmenu)
parent.config(menu=menubar)
def _init_feedback(self, parent):
self._feedbackframe = feedbackframe = Frame(parent)
feedbackframe.pack(fill='x', side='bottom', padx=3, pady=3)
self._lastoper_label = Label(feedbackframe, text='Last Operation:',
font=self._font)
self._lastoper_label.pack(side='left')
lastoperframe = Frame(feedbackframe, relief='sunken', border=1)
lastoperframe.pack(fill='x', side='right', expand=1, padx=5)
self._lastoper1 = Label(lastoperframe, foreground='#007070',
background='#f0f0f0', font=self._font)
self._lastoper2 = Label(lastoperframe, anchor='w', width=30,
foreground='#004040', background='#f0f0f0',
font=self._font)
self._lastoper1.pack(side='left')
self._lastoper2.pack(side='left', fill='x', expand=1)
def _init_canvas(self, parent):
self._cframe = CanvasFrame(parent, background='white',
width=525, closeenough=10,
border=2, relief='sunken')
self._cframe.pack(expand=1, fill='both', side='top', pady=2)
canvas = self._canvas = self._cframe.canvas()
self._stackwidgets = []
self._rtextwidgets = []
self._titlebar = canvas.create_rectangle(0,0,0,0, fill='#c0f0f0',
outline='black')
self._exprline = canvas.create_line(0,0,0,0, dash='.')
self._stacktop = canvas.create_line(0,0,0,0, fill='#408080')
size = self._size.get()+4
self._stacklabel = TextWidget(canvas, 'Stack', color='#004040',
font=self._boldfont)
self._rtextlabel = TextWidget(canvas, 'Remaining Text',
color='#004040', font=self._boldfont)
self._cframe.add_widget(self._stacklabel)
self._cframe.add_widget(self._rtextlabel)
def _redraw(self):
scrollregion = self._canvas['scrollregion'].split()
(cx1, cy1, cx2, cy2) = [int(c) for c in scrollregion]
for stackwidget in self._stackwidgets:
self._cframe.destroy_widget(stackwidget)
self._stackwidgets = []
for rtextwidget in self._rtextwidgets:
self._cframe.destroy_widget(rtextwidget)
self._rtextwidgets = []
(x1, y1, x2, y2) = self._stacklabel.bbox()
y = y2-y1+10
self._canvas.coords(self._titlebar, -5000, 0, 5000, y-4)
self._canvas.coords(self._exprline, 0, y*2-10, 5000, y*2-10)
(x1, y1, x2, y2) = self._stacklabel.bbox()
self._stacklabel.move(5-x1, 3-y1)
(x1, y1, x2, y2) = self._rtextlabel.bbox()
self._rtextlabel.move(cx2-x2-5, 3-y1)
stackx = 5
for tok in self._parser.stack():
if isinstance(tok, parse.Tree):
attribs = {'tree_color': '#4080a0', 'tree_width': 2,
'node_font': self._boldfont,
'node_color': '#006060',
'leaf_color': '#006060', 'leaf_font':self._font}
widget = tree_to_treesegment(self._canvas, tok,
**attribs)
widget.node()['color'] = '#000000'
else:
widget = TextWidget(self._canvas, tok,
color='#000000', font=self._font)
widget.bind_click(self._popup_reduce)
self._stackwidgets.append(widget)
self._cframe.add_widget(widget, stackx, y)
stackx = widget.bbox()[2] + 10
rtextwidth = 0
for tok in self._parser.remaining_text():
widget = TextWidget(self._canvas, tok,
color='#000000', font=self._font)
self._rtextwidgets.append(widget)
self._cframe.add_widget(widget, rtextwidth, y)
rtextwidth = widget.bbox()[2] + 4
if len(self._rtextwidgets) > 0:
stackx += self._rtextwidgets[0].width()
stackx = max(stackx, self._stacklabel.width()+25)
rlabelwidth = self._rtextlabel.width()+10
if stackx >= cx2-max(rtextwidth, rlabelwidth):
cx2 = stackx + max(rtextwidth, rlabelwidth)
for rtextwidget in self._rtextwidgets:
rtextwidget.move(4+cx2-rtextwidth, 0)
self._rtextlabel.move(cx2-self._rtextlabel.bbox()[2]-5, 0)
midx = (stackx + cx2-max(rtextwidth, rlabelwidth))/2
self._canvas.coords(self._stacktop, midx, 0, midx, 5000)
(x1, y1, x2, y2) = self._stacklabel.bbox()
if len(self._rtextwidgets) > 0:
def drag_shift(widget, midx=midx, self=self):
if widget.bbox()[0] < midx: self.shift()
else: self._redraw()
self._rtextwidgets[0].bind_drag(drag_shift)
self._rtextwidgets[0].bind_click(self.shift)
self._highlight_productions()
def _draw_stack_top(self, widget):
midx = widget.bbox()[2]+50
self._canvas.coords(self._stacktop, midx, 0, midx, 5000)
def _highlight_productions(self):
self._prodlist.selection_clear(0, 'end')
for prod in self._parser.reducible_productions():
index = self._productions.index(prod)
self._prodlist.selection_set(index)
def destroy(self, *e):
if self._top is None: return
self._top.destroy()
self._top = None
def reset(self, *e):
self._parser.initialize(self._sent)
self._lastoper1['text'] = 'Reset Demo'
self._lastoper2['text'] = ''
self._redraw()
def step(self, *e):
if self.reduce(): return 1
elif self.shift(): return 1
else:
if len(self._parser.parses()) > 0:
self._lastoper1['text'] = 'Finished:'
self._lastoper2['text'] = 'Success'
else:
self._lastoper1['text'] = 'Finished:'
self._lastoper2['text'] = 'Failure'
def shift(self, *e):
if self._animating_lock: return
if self._parser.shift():
tok = self._parser.stack()[-1]
self._lastoper1['text'] = 'Shift:'
self._lastoper2['text'] = '%r' % tok
if self._animate.get():
self._animate_shift()
else:
self._redraw()
return 1
return 0
def reduce(self, *e):
if self._animating_lock: return
production = self._parser.reduce()
if production:
self._lastoper1['text'] = 'Reduce:'
self._lastoper2['text'] = '%s' % production
if self._animate.get():
self._animate_reduce()
else:
self._redraw()
return production
def undo(self, *e):
if self._animating_lock: return
if self._parser.undo():
self._redraw()
def postscript(self, *e):
self._cframe.print_to_file()
def mainloop(self, *args, **kwargs):
"""
Enter the Tkinter mainloop. This function must be called if
this demo is created from a non-interactive program (e.g.
from a secript); otherwise, the demo will close as soon as
the script completes.
"""
if in_idle(): return
self._top.mainloop(*args, **kwargs)
def resize(self, size=None):
if size is not None: self._size.set(size)
size = self._size.get()
self._font.configure(size=-(abs(size)))
self._boldfont.configure(size=-(abs(size)))
self._sysfont.configure(size=-(abs(size)))
self._redraw()
def help(self, *e):
try:
ShowText(self._top, 'Help: Chart Parser Demo',
(__doc__).strip(), width=75, font='fixed')
except:
ShowText(self._top, 'Help: Chart Parser Demo',
(__doc__).strip(), width=75)
def about(self, *e):
ABOUT = ("NLTK Shift-Reduce Parser Demo\n"+
"Written by Edward Loper")
TITLE = 'About: Shift-Reduce Parser Demo'
try:
from tkMessageBox import Message
Message(message=ABOUT, title=TITLE).show()
except:
ShowText(self._top, TITLE, ABOUT)
def edit_grammar(self, *e):
CFGEditor(self._top, self._parser.grammar(), self.set_grammar)
def set_grammar(self, grammar):
self._parser.set_grammar(grammar)
self._productions = list(grammar.productions())
self._prodlist.delete(0, 'end')
for production in self._productions:
self._prodlist.insert('end', (' %s' % production))
def edit_sentence(self, *e):
sentence = string.join(self._sent)
title = 'Edit Text'
instr = 'Enter a new sentence to parse.'
EntryDialog(self._top, sentence, instr, self.set_sentence, title)
def set_sentence(self, sentence):
self._sent = sent.split()
self.reset()
def _toggle_grammar(self, *e):
if self._show_grammar.get():
self._prodframe.pack(fill='both', side='left', padx=2,
after=self._feedbackframe)
self._lastoper1['text'] = 'Show Grammar'
else:
self._prodframe.pack_forget()
self._lastoper1['text'] = 'Hide Grammar'
self._lastoper2['text'] = ''
def _prodlist_select(self, event):
selection = self._prodlist.curselection()
if len(selection) != 1: return
index = int(selection[0])
production = self._parser.reduce(self._productions[index])
if production:
self._lastoper1['text'] = 'Reduce:'
self._lastoper2['text'] = '%s' % production
if self._animate.get():
self._animate_reduce()
else:
self._redraw()
else:
self._prodlist.selection_clear(0, 'end')
for prod in self._parser.reducible_productions():
index = self._productions.index(prod)
self._prodlist.selection_set(index)
def _popup_reduce(self, widget):
productions = self._parser.reducible_productions()
if len(productions) == 0: return
self._reduce_menu.delete(0, 'end')
for production in productions:
self._reduce_menu.add_command(label=str(production),
command=self.reduce)
self._reduce_menu.post(self._canvas.winfo_pointerx(),
self._canvas.winfo_pointery())
def _animate_shift(self):
widget = self._rtextwidgets[0]
right = widget.bbox()[0]
if len(self._stackwidgets) == 0: left = 5
else: left = self._stackwidgets[-1].bbox()[2]+10
dt = self._animate.get()
dx = (left-right)*1.0/dt
self._animate_shift_frame(dt, widget, dx)
def _animate_shift_frame(self, frame, widget, dx):
if frame > 0:
self._animating_lock = 1
widget.move(dx, 0)
self._top.after(10, self._animate_shift_frame,
frame-1, widget, dx)
else:
del self._rtextwidgets[0]
self._stackwidgets.append(widget)
self._animating_lock = 0
self._draw_stack_top(widget)
self._highlight_productions()
def _animate_reduce(self):
numwidgets = len(self._parser.stack()[-1])
widgets = self._stackwidgets[-numwidgets:]
if isinstance(widgets[0], TreeSegmentWidget):
ydist = 15 + widgets[0].node().height()
else:
ydist = 15 + widgets[0].height()
dt = self._animate.get()
dy = ydist*2.0/dt
self._animate_reduce_frame(dt/2, widgets, dy)
def _animate_reduce_frame(self, frame, widgets, dy):
if frame > 0:
self._animating_lock = 1
for widget in widgets: widget.move(0, dy)
self._top.after(10, self._animate_reduce_frame,
frame-1, widgets, dy)
else:
del self._stackwidgets[-len(widgets):]
for widget in widgets:
self._cframe.remove_widget(widget)
tok = self._parser.stack()[-1]
if not isinstance(tok, parse.Tree): raise ValueError()
label = TextWidget(self._canvas, str(tok.node), color='#006060',
font=self._boldfont)
widget = TreeSegmentWidget(self._canvas, label, widgets,
width=2)
(x1, y1, x2, y2) = self._stacklabel.bbox()
y = y2-y1+10
if not self._stackwidgets: x = 5
else: x = self._stackwidgets[-1].bbox()[2] + 10
self._cframe.add_widget(widget, x, y)
self._stackwidgets.append(widget)
self._draw_stack_top(widget)
self._highlight_productions()
self._animating_lock = 0
def _highlight_hover(self, event):
index = self._prodlist.nearest(event.y)
if self._hover == index: return
self._clear_hover()
selection = [int(s) for s in self._prodlist.curselection()]
if index in selection:
rhslen = len(self._productions[index].rhs())
for stackwidget in self._stackwidgets[-rhslen:]:
if isinstance(stackwidget, TreeSegmentWidget):
stackwidget.node()['color'] = '#00a000'
else:
stackwidget['color'] = '#00a000'
self._hover = index
def _clear_hover(self, *event):
if self._hover == -1: return
self._hover = -1
for stackwidget in self._stackwidgets:
if isinstance(stackwidget, TreeSegmentWidget):
stackwidget.node()['color'] = 'black'
else:
stackwidget['color'] = 'black'
def demo():
"""
Create a shift reduce parser demo, using a simple grammar and
text.
"""
from nltk import cfg
nonterminals = 'S VP NP PP P N Name V Det'
(S, VP, NP, PP, P, N, Name, V, Det) = [cfg.Nonterminal(s)
for s in nonterminals.split()]
productions = (
cfg.Production(S, [NP, VP]),
cfg.Production(NP, [Det, N]),
cfg.Production(NP, [NP, PP]),
cfg.Production(VP, [VP, PP]),
cfg.Production(VP, [V, NP, PP]),
cfg.Production(VP, [V, NP]),
cfg.Production(PP, [P, NP]),
cfg.Production(NP, ['I']), cfg.Production(Det, ['the']),
cfg.Production(Det, ['a']), cfg.Production(N, ['man']),
cfg.Production(V, ['saw']), cfg.Production(P, ['in']),
cfg.Production(P, ['with']), cfg.Production(N, ['park']),
cfg.Production(N, ['dog']), cfg.Production(N, ['statue']),
cfg.Production(Det, ['my']),
)
grammar = cfg.Grammar(S, productions)
sent = 'my dog saw a man in the park with a statue'.split()
ShiftReduceDemo(grammar, sent).mainloop()
if __name__ == '__main__': demo()