ironlute-cvs Mailing List for Iron Lute
Status: Pre-Alpha
Brought to you by:
thejerf
You can subscribe to this list here.
2004 |
Jan
|
Feb
|
Mar
|
Apr
|
May
|
Jun
(1) |
Jul
|
Aug
|
Sep
|
Oct
|
Nov
|
Dec
|
---|---|---|---|---|---|---|---|---|---|---|---|---|
2005 |
Jan
|
Feb
|
Mar
(46) |
Apr
(2) |
May
(5) |
Jun
|
Jul
|
Aug
|
Sep
|
Oct
|
Nov
|
Dec
|
From: Jeremy B. <th...@us...> - 2005-05-08 20:57:23
|
Update of /cvsroot/ironlute/ironlute/gtk In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv16922 Modified Files: handletag.py outlinewidget.py Log Message: Towards functional outliner; got some stuff in here I wouldn't care to replicate if I lose it. Index: outlinewidget.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/outlinewidget.py,v retrieving revision 1.3 retrieving revision 1.4 diff -C2 -d -r1.3 -r1.4 *** outlinewidget.py 7 May 2005 20:11:52 -0000 1.3 --- outlinewidget.py 8 May 2005 20:57:14 -0000 1.4 *************** *** 2,5 **** --- 2,26 ---- * Change philosophy. (I think that I have it covered, but I might be wrong. + + Buffer Hypothesis: + + This is just a hypothesis but it seems to fit the facts. + + The buffer seems to store lines as first-class citizens in its data + models. When you request the 5th line, you really get the fifth line. + + Iterators and marks, on the other hand, seem derived, despite what + you'd think. A mark is only updated when the GUI regains control; if + you insert some text before a mark, then request the iterator for that + mark, you may not get what you expect. + + In particular, I had a problem with creating new lines, and not being + able to retrieve the nodes after those lines until after the GUI + updated itself. + + This explains why some parts of this code still play with line + numbers; you can't keep retrieving the iterator for the next node, + because it hasn't moved yet, and you end up with all the nodes being + inserted backwards. """ *************** *** 29,32 **** --- 50,55 ---- class OutlineWidget(gtk.TextView, guiSupport.IronLuteOutlineFrame): def __init__(self, rootHandle, **kw): + self.magic = False + guiSupport.IronLuteOutlineFrame.__init__(self, initialView = rootHandle) *************** *** 48,52 **** self.insert = it self.selection_bound = it - # event handlers self.connect("key-press-event", self.keypress) --- 71,74 ---- *************** *** 63,68 **** changed.""" # If the change is user initiated, it has to be near the ! # cursor. ! self.getCurrentHandleTag() def selectionEvent(self, source, event): --- 85,90 ---- changed.""" # If the change is user initiated, it has to be near the ! # cursor... I hope. ! self.getCurrentHandleTag().updateNodeFromWidget() def selectionEvent(self, source, event): *************** *** 149,154 **** except StopIteration: pass # use initial newPosIter ! lineNum = newPosIter.get_line() ! HandleTag(self, handle, newPosIter, lineNum) print "Unhandled event", name, ":", event.__dict__ --- 171,175 ---- except StopIteration: pass # use initial newPosIter ! HandleTag(self, handle, newPosIter) print "Unhandled event", name, ":", event.__dict__ *************** *** 226,230 **** i = 0 for handle, _ in self.handleWalkerForward(self.rootHandle[0]): ! HandleTag(self, handle, buffer.get_end_iter(), i) i += 1 --- 247,251 ---- i = 0 for handle, _ in self.handleWalkerForward(self.rootHandle[0]): ! HandleTag(self, handle, buffer.get_end_iter()) i += 1 *************** *** 260,263 **** --- 281,299 ---- "no children.") + # Get the node we're inserting everything in front of, + # or None if it's at the end + print "Source:", handleToChange.node.getData() + it = self.handleWalkerForward(handleToChange) + it.next() + try: + insertTagHandle, _ = it.next() + print "Inserting before:", insertTagHandle.node.getData() + insertTag = self.handleToTag[insertTagHandle] + print insertTag.text + insertLine = insertTag.widgetBegin.get_line() + print "Inserting at line", insertLine + except IndexError: + insertLine = None + self.handleStates[handleToChange] = constants.OPEN *************** *** 268,285 **** return - lineNum = handleTag.getLineNumber() - it = self.handleWalkerForward(handleToChange, True) # skip the first one, as it is handleToChange it.next() - thisLine = lineNum + 1 for handle, depth in it: if handle in self.handleToTag: # Remove the old tag if it exists, paranoia self.handleToTag[handle].destroy() ! HandleTag(self, handle, ! self.buffer.get_iter_at_line(thisLine), ! thisLine) ! thisLine += 1 if state == constants.CLOSED: --- 304,320 ---- return it = self.handleWalkerForward(handleToChange, True) # skip the first one, as it is handleToChange it.next() for handle, depth in it: if handle in self.handleToTag: # Remove the old tag if it exists, paranoia self.handleToTag[handle].destroy() ! if insertLine is None: ! startIter = self.buffer.get_end_iter() ! else: ! startIter = self.buffer.get_iter_at_line(insertLine) ! insertLine += 1 ! HandleTag(self, handle, startIter) if state == constants.CLOSED: *************** *** 292,297 **** if wasVisible: - import pdb - #pdb.set_trace() it = self.handleWalkerForward(handleToChange, True) it.next() --- 327,330 ---- *************** *** 373,374 **** --- 406,454 ---- # is high, but if nobody says they want Tk, this will go return + + def handleChildAdded(self, sourceHandle, link): + """Recieves a childAdded event from a HandleTag and handle it. + + @param sourceHandle: The handle the event was emitted + from. The new node is a child of that handle. + @param link: The new link that was added for the new node; the + new node is link.dest.""" + + # There are three possibilities, one for each possible + # node state. + # We already know the source handle is *visible*, or we wouldn't + # have a HandleTag watching a LinkHandle to recieve + # this event. + + state = self.handleStates[sourceHandle] + + if state == constants.NOCHILDREN: + # We're not going to display this new node, but we will + # update the indicator. + self.handleStates[sourceHandle] = constants.CLOSED + indicator = self.handleToTag[sourceHandle].indicator + indicator.setState(constants.CLOSED) + return + + if state == constants.CLOSED: + # We're not displaying anything, so ignore it + return + + if state == constants.OPEN: + # We have to add the new handle in. + newChildHandle = sourceHandle.getHandleForLink(link) + # Now, we have to figure out the new position, which is + # the current position of the next line, or the end of the + # buffer + walker = self.handleWalkerForward(newChildHandle) + walker.next() + try: + nextHandle, _ = walker.next() + newPosIter = self.handleToTag[nextHandle].getWidgetBeginIter() + except StopIteration: + newPosIter = self.buffer.get_end_iter() + HandleTag(self, newChildHandle, newPosIter) + return + + raise ValueError("Can't add new child because source handle " + "has unknown state: %s" % handleState) Index: handletag.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/handletag.py,v retrieving revision 1.3 retrieving revision 1.4 diff -C2 -d -r1.3 -r1.4 *** handletag.py 7 May 2005 20:11:52 -0000 1.3 --- handletag.py 8 May 2005 20:57:14 -0000 1.4 *************** *** 25,33 **** class HandleTag(object): ! def __init__(self, outlineWidget, handle, myIter, lineNum): self.tagNumber = tagNumbers() self.handle = handle self.buffer = outlineWidget.buffer self.outlineWidget = outlineWidget --- 25,46 ---- class HandleTag(object): ! def __init__(self, outlineWidget, handle, myIter): self.tagNumber = tagNumbers() self.handle = handle + lineNum = myIter.get_line() + self.buffer = outlineWidget.buffer + # Make us a new line + newlineIter = self.buffer.get_iter_at_line(lineNum) + newlineIter.backward_char() + self.buffer.insert(newlineIter, "\n") + myIter = self.buffer.get_iter_at_line(lineNum) + + # Mark the iter position, since we'll want to use + # it a lot + self.mark = self.buffer.create_mark(str(self) + "_mark", + myIter, True) + self.mark.set_visible(True) self.outlineWidget = outlineWidget *************** *** 41,45 **** textToInsert = handle.getNode().getData() ! self.buffer.insert(myIter, textToInsert + "\n") self.tag.set_property("size-points", 15) --- 54,62 ---- textToInsert = handle.getNode().getData() ! self.buffer.insert(self.widgetBegin, textToInsert) ! widgetEnd = self.widgetBegin ! widgetEnd.forward_to_line_end() ! widgetEnd.forward_char() ! #self.buffer.remove_all_tags(self.widgetBegin, widgetEnd) self.tag.set_property("size-points", 15) *************** *** 52,56 **** # Set up the indicator ! anchorIter = self.buffer.get_iter_at_line(lineNum) self.anchor = self.buffer.create_child_anchor(anchorIter) self.indicator = Indicator(self, handle, pixels) --- 69,73 ---- # Set up the indicator ! anchorIter = self.widgetBegin self.anchor = self.buffer.create_child_anchor(anchorIter) self.indicator = Indicator(self, handle, pixels) *************** *** 74,87 **** self.indicator.setState(self.outlineWidget.handleStates[self.handle]) ! self.buffer.apply_tag(self.tag, ! self.buffer.get_iter_at_line(lineNum), ! self.buffer.get_iter_at_line(lineNum + ! 1)) ! ! # Set a mark, which we use to retrieve line numbers and such ! lineIter = self.buffer.get_iter_at_line(lineNum) ! self.mark = self.buffer.create_mark(str(self) + "_mark", ! lineIter, True) ! self.handle.subscribe(self) --- 91,98 ---- self.indicator.setState(self.outlineWidget.handleStates[self.handle]) ! tagEnd = self.widgetBegin ! tagEnd.forward_to_line_end() ! tagEnd.forward_char() ! self.buffer.apply_tag(self.tag, self.widgetBegin, tagEnd) self.handle.subscribe(self) *************** *** 96,100 **** To activate magic, uncomment the 'magic' part of the keypress handler in outlinewidget.""" ! self.setText("Sophie is a good dog.") def getState(self): --- 107,118 ---- To activate magic, uncomment the 'magic' part of the keypress handler in outlinewidget.""" ! self.mark.set_visible(False) ! print self.outlineWidget.insert.get_line() ! print self.text ! print id(self) ! print str(self) ! #print self.outlineWidget.insert.get_tags() ! #self.outlineWidget.magic = True ! #self.handle.node.addNewChild() def getState(self): *************** *** 117,122 **** data_change. This directly handles the last, and indirectly the first two.""" print event ! print self.getText() def unselect(self): --- 135,152 ---- data_change. This directly handles the last, and indirectly the first two.""" + try: + if event.source is self: + return + except AttributeError: pass + print event ! ! if event.eventName == "data_change" and not hasattr(event, "key"): ! self.updateWidgetFromNode() ! ! # Things we have to pass up to the outline widget ! if event.eventName == "child_added": ! self.outlineWidget.handleChildAdded(event.sourceHandle, ! event.link) def unselect(self): *************** *** 135,139 **** """Sets the position of the cursor within this tag, as a function of characters after the node indicator.""" ! it = self.getWidgetBeginIter() it.forward_chars(pos + 1) self.outlineWidget.insert = it --- 165,169 ---- """Sets the position of the cursor within this tag, as a function of characters after the node indicator.""" ! it = self.widgetBegin it.forward_chars(pos + 1) self.outlineWidget.insert = it *************** *** 142,150 **** def updateNodeFromWidget(self): """Copy the text from this widget into the node.""" ! self.handle.node.setData(self.getText()) def updateWidgetFromNode(self): """Copy the text in the node out to the widget.""" ! self.setText(self.handle.node.getData()) def toggle(self): --- 172,182 ---- def updateNodeFromWidget(self): """Copy the text from this widget into the node.""" ! self.handle.node.setData(self.text, source = self) def updateWidgetFromNode(self): """Copy the text in the node out to the widget.""" ! print "updated from node" ! self.text = self.handle.node.getData() ! print self.handle.node.getData() def toggle(self): *************** *** 163,169 **** Note using this invalidates GtkTextIters.""" del self.outlineWidget.gtkTagToHandleTag[self.tag] ! begin = self.getWidgetBeginIter() # just after the indicator #begin.backward_char() # back up to get the indicator ! end = self.getWidgetBeginIter() end.forward_to_line_end() # end of line end.forward_char() # get the CRLF too --- 195,201 ---- Note using this invalidates GtkTextIters.""" del self.outlineWidget.gtkTagToHandleTag[self.tag] ! begin = self.widgetBegin # just after the indicator #begin.backward_char() # back up to get the indicator ! end = self.widgetBegin end.forward_to_line_end() # end of line end.forward_char() # get the CRLF too *************** *** 177,180 **** --- 209,214 ---- return self.buffer.get_iter_at_mark(self.mark) + widgetBegin = property(getWidgetBeginIter) + def getTextStartIter(self): """Gets an iterator set at the start of the text. *************** *** 183,193 **** start of the node; I want to make it possible to have multiple little widgets there if necessary.""" ! it = self.getWidgetBeginIter() it.forward_char() return it ! def getLineNumber(self): ! """Returns the current line number of the handle.""" ! return self.getWidgetBeginIter().get_line() def bounds(self): --- 217,225 ---- start of the node; I want to make it possible to have multiple little widgets there if necessary.""" ! it = self.widgetBegin it.forward_char() return it ! textBegin = property(getTextStartIter) def bounds(self): *************** *** 197,201 **** @returns TextIter, TextIter (tuple)""" ! bounds = self.getWidgetBeginIter(), self.getWidgetBeginIter() # FIXME: I end up doing this a lot, either factor or isolate bounds[1].forward_to_line_end() --- 229,233 ---- @returns TextIter, TextIter (tuple)""" ! bounds = self.widgetBegin, self.widgetBegin # FIXME: I end up doing this a lot, either factor or isolate bounds[1].forward_to_line_end() *************** *** 210,214 **** @returns GtkTextIter, GtkTextIter (tuple)""" ! bounds = self.getTextStartIter(), self.getWidgetBeginIter() bounds[1].forward_to_line_end() return bounds --- 242,246 ---- @returns GtkTextIter, GtkTextIter (tuple)""" ! bounds = self.textBegin, self.widgetBegin bounds[1].forward_to_line_end() return bounds *************** *** 226,230 **** """Sets this node as pure text.""" self.clear() ! self.buffer.insert(self.getTextStartIter(), text) def setFocus(self): --- 258,264 ---- """Sets this node as pure text.""" self.clear() ! self.buffer.insert(self.textBegin, text) ! ! text = property(getText, setText, clear) def setFocus(self): *************** *** 237,244 **** def selectAll(self): ! itBegin = self.getWidgetBeginIter() ! itBegin.forward_char() self.outlineWidget.selection_bound = itBegin ! itEnd = self.getWidgetBeginIter() itEnd.forward_to_line_end() self.outlineWidget.insert = itEnd --- 271,277 ---- def selectAll(self): ! itBegin = self.textBegin self.outlineWidget.selection_bound = itBegin ! itEnd = self.widgetBegin itEnd.forward_to_line_end() self.outlineWidget.insert = itEnd |
From: Jeremy B. <th...@us...> - 2005-05-07 20:12:02
|
Update of /cvsroot/ironlute/ironlute/gtk In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv20093 Modified Files: IronLuteGtk.py gtkmenubar.py handletag.py outlinewidget.py Log Message: General commit, just to back things up. Index: gtkmenubar.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/gtkmenubar.py,v retrieving revision 1.2 retrieving revision 1.3 diff -C2 -d -r1.2 -r1.3 *** gtkmenubar.py 4 May 2005 20:32:03 -0000 1.2 --- gtkmenubar.py 7 May 2005 20:11:52 -0000 1.3 *************** *** 86,96 **** None, state = True): """See command.py/CommandMenu.""" ! item = gtk.MenuItem(command.name) ! # FIXME: Add a right-aligned label for the accelstring item.set_sensitive(state) item.show() ! def doCommand(): self.outlineWidget.doCommand(command, self) item.connect("activate", doCommand) --- 86,110 ---- None, state = True): """See command.py/CommandMenu.""" + item = gtk.MenuItem() ! hbox = gtk.HBox() ! l = gtk.Label(command.name) ! hbox.pack_start(l, False, False, 10) ! l.show() ! l = gtk.Label() ! hbox.pack_start(l, True, True, 0) ! l.show() ! if accelString: ! accel = gtk.Label("test") ! hbox.pack_end(accel, False, False, 0) ! accel.show() ! hbox.show() ! item.add(hbox) ! ! # FIXME: Accelerator strings item.set_sensitive(state) item.show() ! def doCommand(source): self.outlineWidget.doCommand(command, self) item.connect("activate", doCommand) Index: IronLuteGtk.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/IronLuteGtk.py,v retrieving revision 1.2 retrieving revision 1.3 diff -C2 -d -r1.2 -r1.3 *** IronLuteGtk.py 4 May 2005 20:32:03 -0000 1.2 --- IronLuteGtk.py 7 May 2005 20:11:52 -0000 1.3 *************** *** 1,2 **** --- 1,4 ---- + + # NEXT: Connect the events to the outline. import pygtk pygtk.require("2.0") *************** *** 29,33 **** def GtkEndApp(*args, **kwargs): - # FIXME: What insta-kills the app? gtk.main_quit() --- 31,34 ---- *************** *** 67,72 **** self.menubar.show() self.outlineWidget.menubar.checkActive() ! self.vbox.add(self.menubar) self.vbox.add(self.sw) --- 68,74 ---- self.menubar.show() self.outlineWidget.menubar.checkActive() + self.outlineWidget.grab_focus() ! self.vbox.pack_start(self.menubar, False) self.vbox.add(self.sw) *************** *** 82,90 **** 'Cheesy poofs', 'tart and tangy']*20) rootHandle = document.getRootHandle() ! if __name__ == "__main__": GtkStartInit() myWidget = IronLuteGtk(rootHandle) GtkFinishInit() --- 84,94 ---- 'Cheesy poofs', 'tart and tangy']*20) rootHandle = document.getRootHandle() ! if __name__ == "__main__": GtkStartInit() + #import profile myWidget = IronLuteGtk(rootHandle) GtkFinishInit() + Index: outlinewidget.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/outlinewidget.py,v retrieving revision 1.2 retrieving revision 1.3 diff -C2 -d -r1.2 -r1.3 *** outlinewidget.py 4 May 2005 20:32:03 -0000 1.2 --- outlinewidget.py 7 May 2005 20:11:52 -0000 1.3 *************** *** 1,2 **** --- 1,9 ---- + """To document: + + * Change philosophy. (I think that I have it covered, but I might be wrong. + """ + + # Note: 0x416 is a russian letter, good for unicode testing + import gtk *************** *** 29,50 **** self.handleToTag = {} self.gtkTagToHandleTag = {} self.nameToTag = {} self.rootHandle = rootHandle self.bindRootHandle() self.connect("key-press-event", self.keypress) ! self.connect("move-cursor", self.cursorMove) buffer = property(gtk.TextView.get_buffer) def keypress(self, source, event): ! print event.keyval - def cursorMove(self, source, step, count, extendSelection, data = None): - print "Step:", step - print "count", count - print "sel", extendSelection - # We have to check the dynamic menus here self.menubar.checkActive() --- 36,223 ---- self.handleToTag = {} + # FIXME: This to satisfy guiSupport + self.handlesToWidgets = self.handleToTag self.gtkTagToHandleTag = {} self.nameToTag = {} self.rootHandle = rootHandle + self.rootHandle.node.getDocument().subscribe(self) self.bindRootHandle() + it = self.buffer.get_start_iter() + it.forward_char() + self.insert = it + self.selection_bound = it + + # event handlers self.connect("key-press-event", self.keypress) ! self.connect_after("move-cursor", self.cursorMove) ! self.connect_after("select-all", self.selectAll) ! self.buffer.connect("changed", self.changed) ! self.buffer.connect("insert-text", self.insertText) ! self.connect("button-release-event", self.clickFix) ! # This handle mouse-drag selection ! self.connect_after("motion-notify-event", self.selectionEvent) ! ! def changed(self, *args): ! """This handles the notification that something in the buffer ! changed.""" ! # If the change is user initiated, it has to be near the ! # cursor. ! self.getCurrentHandleTag() ! ! def selectionEvent(self, source, event): ! """Handle the mouse-notify-event, if the user is selecting ! with the mouse. ! ! This validates the user can't select over node boundaries. ! ! During selection, the selection_bound remains stationary, and ! the insert bound moves. If the insert bound changes lines, or ! grows to include zero, we ban the motion.""" ! ! insert = self.insert ! selection_bound = self.selection_bound ! selection_line = selection_bound.get_line() ! insert_line = insert.get_line() ! ! if insert_line < selection_line: ! # Set the insertion point to the beginning of the line + ! # 1, ban motion ! new_insert = selection_bound.copy() ! new_insert.backward_chars(new_insert.get_line_offset() - 1) ! self.insert = new_insert ! ! # FIXME: last line ! if insert_line > selection_line: ! # Set the insertion point to the end of the line, ban ! # motion ! new_insert = selection_bound.copy() ! new_insert.forward_line() ! new_insert.backward_char() ! self.insert = new_insert ! ! if insert.get_line_offset() == 0: ! print "he" ! insert = insert.copy() ! insert.forward_char() ! self.insert = insert ! ! def fixCursor(self): ! """Validate the cursor is not on the wrong side of the node ! indicator.""" ! insert = self.insert ! if insert.get_line_offset() == 0: ! insert.forward_char() ! self.insert = insert ! self.selection_bound = insert ! ! def clickFix(self, source, event): ! if self.insert.equal(self.selection_bound): ! self.fixCursor() ! ! ! def insertText(self, source, pos, text, charCount): ! pass ! ! def notifyHandleEvent(self, event): ! """Handle events coming from the outline document.""" ! print event ! handle = event.sourceHandle ! # If the source is invisible, no problem. ! if not self.isVisible(handle): ! return ! ! node = event.source ! eventName = event.name ! if name == "child_removed": ! # The node is about to be gone, remove the handletag ! handleTag = self.handleToTag[handle] ! handleTag.destroy() ! return ! ! if name == "child_added": ! # There is a new node in town, add the handle tag for the ! # new handle. It gets inserted right in front of the next ! # handle, or at the end if there isn't one ! newPosIter = self.buffer.get_end_iter() ! try: ! handleWalker = self.handleWalkerForward(handle) ! handleWalker.next() # skip handle itself ! nextHandle, _ = handleWalker.next() ! nextHandleTag = self.handleToTag[nextHandle] ! newPosIter = nextHandleTag.getMarkIter() ! except StopIteration: pass # use initial newPosIter ! ! lineNum = newPosIter.get_line() ! HandleTag(self, handle, newPosIter, lineNum) ! ! print "Unhandled event", name, ":", event.__dict__ buffer = property(gtk.TextView.get_buffer) def keypress(self, source, event): ! # CTRL-m does debugging magic ! if event.keyval == 109 and event.state & gtk.gdk.CONTROL_MASK: ! self.getCurrentHandleTag().magic() ! return True ! ! ! def selectAll(self, source, selecting): ! """For Select All events, we re-write it to select the entire ! text of the current paragraph.""" ! if selecting: ! self.currentHandleTag.selectAll() ! return True ! ! def cursorMove(self, source, step, count, extendSelection, data = ! None): ! """This validates the cursor stays in legal positions while ! being moved. Basically, you're not allowed to be on the left ! of the node indicator, and so we keep you moving past it if ! that's where you are. ! ! We also validate the dynamic menus against the current ! position.""" ! print "Move" ! currentOffset = self.insert.get_line_offset() ! ! if currentOffset != 0: ! self.menubar.checkActive() ! return ! ! # There are a lot of cases, but in each case, the answer is ! # the same: Go forward or backward one char ! # ! def goForward(): ! it.forward_char() ! self.insert = it ! self.selection_bound = it ! def goBackward(): ! it.backward_char() ! self.insert = it ! self.selection_bound = it ! ! it = self.insert ! ! # The basic goal here is to get the cursor out from behind ! # the node indicator in the fashion the user will expect ! # as if the node indicator wasn't part of the text widget. ! # The comments will confine themselves to describing the ! # situation, as the resolution should be obvious after that: ! if step == gtk.MOVEMENT_VISUAL_POSITIONS: # left/right ! if count == 1: # right ! # FIXME: What if this is the last line? ! goForward() ! if count == -1: # left ! if self.insert.get_line() == 0: ! # on the very first line ! goForward() ! else: ! goBackward() ! else: ! # for anything else, assume we jumped to the node, ! # go forward ! goForward() self.menubar.checkActive() *************** *** 135,139 **** return self.buffer.get_iter_at_mark(cursorMark) ! insert = property(getInsert) def getSelectionBound(self): --- 308,316 ---- return self.buffer.get_iter_at_mark(cursorMark) ! def setInsert(self, iter): ! """Sets the insertion point to the given iter.""" ! self.buffer.move_mark(self.buffer.get_insert(), iter) ! ! insert = property(getInsert, setInsert) def getSelectionBound(self): *************** *** 145,149 **** return self.buffer.get_iter_at_mark(selMark) ! selection_bound = property(getSelectionBound) def getCurrentHandleTag(self): --- 322,330 ---- return self.buffer.get_iter_at_mark(selMark) ! def setSelectionBound(self, iter): ! """Sets the selection bound to the given iter.""" ! self.buffer.move_mark(self.buffer.get_selection_bound(), iter) ! ! selection_bound = property(getSelectionBound, setSelectionBound) def getCurrentHandleTag(self): *************** *** 169,176 **** currentFocus = property(getCurrentFocus) ! def getSelection(self, sourceHandleTag = None): """Returns the current selection as an outline.Cursor.""" ! if sourceHandleTag is None: sourceHandleTag = self.currentHandleTag return outline.Cursor(sourceHandleTag.handle, --- 350,359 ---- currentFocus = property(getCurrentFocus) ! def getSelection(self, sourceHandle = None): """Returns the current selection as an outline.Cursor.""" ! if sourceHandle is None: sourceHandleTag = self.currentHandleTag + else: + sourceHandleTag = self.handleToTag[sourceHandle] return outline.Cursor(sourceHandleTag.handle, *************** *** 179,181 **** --- 362,374 ---- selection = property(getSelection) + + def ensureVisible(self, handle): + """Ensures the current handle tag is visible.""" + handleTag = self.handleToTag[handle] + self.scroll_to_iter(handleTag.getMarkIter(), 0) + def syncWithDocument(self, *args, **kwargs): + # This is a holdover from the *first* Tk implemenatation; + # I'm leaving it in for now because the replacement cost + # is high, but if nobody says they want Tk, this will go + return Index: handletag.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/handletag.py,v retrieving revision 1.2 retrieving revision 1.3 diff -C2 -d -r1.2 -r1.3 *** handletag.py 4 May 2005 20:32:03 -0000 1.2 --- handletag.py 7 May 2005 20:11:52 -0000 1.3 *************** *** 46,49 **** --- 46,50 ---- size = self.tag.get_property("size") pixels = pango.PIXELS(size) + pixels = 12 self.tag.set_property("left-margin", indent) *************** *** 83,86 **** --- 84,101 ---- lineIter, True) + + self.handle.subscribe(self) + + def magic(self): + """Magic is a method that is used for debugging. + + Magic is an easy way to debug the handle tags, by making them + do "something" on demand, via magic. Whatever may happen to be + committed at the moment is irrelevant. + + To activate magic, uncomment the 'magic' part of the keypress + handler in outlinewidget.""" + self.setText("Sophie is a good dog.") + def getState(self): """Retrieves the open/closed/nochildren state from the outline *************** *** 95,98 **** --- 110,151 ---- state = property(getState, setState) + def notifyHandleEvent(self, event): + """Handles an event emitted by a Handle. (OK, sorry about the + word confusion there... + + There are three basic events: child_removed, child_added, and + data_change. This directly handles the last, and indirectly + the first two.""" + print event + print self.getText() + + def unselect(self): + """Unselect the text. + + Strictly speaking, this is a holdover from the 'one widget per + handle' model, but it can live here.""" + self.outlineWidget.selection_bound = self.outlineWidget.insert + + def getCursorPos(self): + """Returns the index of the char the cursor appears before.""" + pos = self.outlineWidget.insert.get_line_offset() - 1 + return pos + + def setCursorPos(self, pos): + """Sets the position of the cursor within this tag, as a + function of characters after the node indicator.""" + it = self.getWidgetBeginIter() + it.forward_chars(pos + 1) + self.outlineWidget.insert = it + self.outlineWidget.selection_bound = it + + def updateNodeFromWidget(self): + """Copy the text from this widget into the node.""" + self.handle.node.setData(self.getText()) + + def updateWidgetFromNode(self): + """Copy the text in the node out to the widget.""" + self.setText(self.handle.node.getData()) + def toggle(self): """Toggles this node's expansion state, which just gets sent *************** *** 110,148 **** Note using this invalidates GtkTextIters.""" del self.outlineWidget.gtkTagToHandleTag[self.tag] ! begin = self.getMarkIter() # just after the indicator #begin.backward_char() # back up to get the indicator ! end = self.getMarkIter() end.forward_to_line_end() # end of line end.forward_char() # get the CRLF too self.buffer.delete(begin, end) ! self.outlineWidget.delete_mark(self.mark) ! def getMarkIter(self): return self.buffer.get_iter_at_mark(self.mark) def getLineNumber(self): """Returns the current line number of the handle.""" ! return self.getMarkIter().get_line() def bounds(self): ! """Returns an GtkTextIter covering from the beginning to the ! end of this widget. ! ! @param withIndicator If true, the range returned includes the ! indicator. Defaults to false. @returns TextIter, TextIter (tuple)""" ! return self.getMarkIter(), self.getMarkIter().forward_to_line_end() def clear(self): """Removes all text from this handle, leaving the indicator behind.""" ! self.buffer.delete(*self.bounds()) def getText(self): """Get this node as pure text.""" ! return self.buffer.get_text(*self.bounds()) def __str__(self): return "node_%s" % self.tagNumber --- 163,244 ---- Note using this invalidates GtkTextIters.""" del self.outlineWidget.gtkTagToHandleTag[self.tag] ! begin = self.getWidgetBeginIter() # just after the indicator #begin.backward_char() # back up to get the indicator ! end = self.getWidgetBeginIter() end.forward_to_line_end() # end of line end.forward_char() # get the CRLF too self.buffer.delete(begin, end) ! self.buffer.delete_mark(self.mark) ! self.handle.unsubscribe(self) ! ! def getWidgetBeginIter(self): return self.buffer.get_iter_at_mark(self.mark) + def getTextStartIter(self): + """Gets an iterator set at the start of the text. + + Do not try to assume that the node indicator is one after the + start of the node; I want to make it possible to have multiple + little widgets there if necessary.""" + it = self.getWidgetBeginIter() + it.forward_char() + return it + def getLineNumber(self): """Returns the current line number of the handle.""" ! return self.getWidgetBeginIter().get_line() def bounds(self): ! """Returns a pair of GtkTextIters covering from the beginning ! of the widget, including the node indicator, to the end of ! this widget. @returns TextIter, TextIter (tuple)""" ! bounds = self.getWidgetBeginIter(), self.getWidgetBeginIter() ! # FIXME: I end up doing this a lot, either factor or isolate ! bounds[1].forward_to_line_end() ! return bounds ! ! def textBounds(self): ! """Returns a pair of GtkTextIters covering the text in the ! widget, not including the node indicator. ! ! Note you should not try to skip over the indicator yourself, ! as it may or may not be confined to one char. ! ! @returns GtkTextIter, GtkTextIter (tuple)""" ! bounds = self.getTextStartIter(), self.getWidgetBeginIter() ! bounds[1].forward_to_line_end() ! return bounds def clear(self): """Removes all text from this handle, leaving the indicator behind.""" ! self.buffer.delete(*self.textBounds()) def getText(self): """Get this node as pure text.""" ! return self.buffer.get_text(*self.textBounds()) ! ! def setText(self, text): ! """Sets this node as pure text.""" ! self.clear() ! self.buffer.insert(self.getTextStartIter(), text) ! ! def setFocus(self): ! """Set the focus on the current widget, a do-nothing holdover ! from the multi-widget days.""" ! return def __str__(self): return "node_%s" % self.tagNumber + + def selectAll(self): + itBegin = self.getWidgetBeginIter() + itBegin.forward_char() + self.outlineWidget.selection_bound = itBegin + itEnd = self.getWidgetBeginIter() + itEnd.forward_to_line_end() + self.outlineWidget.insert = itEnd |
From: Jeremy B. <th...@us...> - 2005-05-06 03:58:17
|
Update of /cvsroot/ironlute/ironlute In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv12730 Modified Files: outline.py nodeData.py Log Message: Convert outgoing link list into a linked list, instead of an array. This makes getting the next or previous link a very efficient thing to do, which eliminates an accidental O(n^2) algorithm. Index: nodeData.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/nodeData.py,v retrieving revision 1.1 retrieving revision 1.2 diff -C2 -d -r1.1 -r1.2 *** nodeData.py 10 Mar 2005 04:25:57 -0000 1.1 --- nodeData.py 6 May 2005 03:58:05 -0000 1.2 *************** *** 31,34 **** --- 31,35 ---- import weakref from sets import Set + import types from ilerrors import * *************** *** 48,52 **** self.data = "" self.config = {} ! self.outgoing = [] def getNode(self): --- 49,53 ---- self.data = "" self.config = {} ! self.outgoing = OutgoingLinkManager(self) def getNode(self): *************** *** 61,65 **** def addChild(self, newChild = None, pos = None, link = None): ! """addChild adds an already-existing node as position pos. Either a newChild (node) should be passed in, or a link should --- 62,66 ---- def addChild(self, newChild = None, pos = None, link = None): ! """addChild adds an already-existing node at position pos. Either a newChild (node) should be passed in, or a link should *************** *** 73,80 **** If you pass in a link, addChild will use it and only ! manipulate the source of the link. Otherwise, it will create ! one and use the default handling of the link, which is to add ! it to the end of the incoming list of the dest. (This means ! that it's the primary link if no other link existed before.)""" if newChild is None and link is None: --- 74,85 ---- If you pass in a link, addChild will use it and only ! manipulate the source of the link. ! ! @param newChild: The constructed node to add at the given ! position; either this or link should be passed in. ! @param pos: A position, preferably as a link representing the ! insertion point. ! @param link: The link to add at the given position; either ! this or newChild should be passed in.""" if newChild is None and link is None: *************** *** 118,134 **** if link is None: link = outline.Link() - - # Override default link placement behavior if necessary - if pos is not None: - if pos < 0: - pos += len(self.outgoing) - if pos < 0: - raise ValueError("Can't insert node in illegal " - "position.") - self.outgoing.insert(pos, link) link.dest = newChild # Attach link ! link.source = self.node def getChildIndex(self, node): --- 123,130 ---- if link is None: link = outline.Link() link.dest = newChild # Attach link ! link.setSource(self.node, pos) def getChildIndex(self, node): *************** *** 136,140 **** the children. ! Raises ValueError if the node is not in the children.""" # FIXME: Better implementation? --- 132,140 ---- the children. ! Raises ValueError if the node is not in the children. ! ! This function requires a linear search and is slow.""" ! ! #print "getChildIndex used; slow" # FIXME: Better implementation? *************** *** 151,155 **** def getChildCount(self): ! return len(self.getChildren()) def getChildren(self): --- 151,155 ---- def getChildCount(self): ! return len(self.outgoing) def getChildren(self): *************** *** 211,215 **** This is seperate because it may be possible to know the node has children without knowing how many in certain situations.""" ! return len(self.outgoing) > 0 def getAllData(self): --- 211,215 ---- This is seperate because it may be possible to know the node has children without knowing how many in certain situations.""" ! return bool(self.outgoing) def getAllData(self): *************** *** 236,237 **** --- 236,404 ---- return False + + class OutgoingLinkManager(object): + """The default Outgoing Link Manager for the outline node. + + The outgoing link manager objects aggregates functionality + relating to the doubly-linked list of outgoing links. It provides + a link to the head and tail of the list, and an iterator that can + walk it (assuming no changes during iteration).""" + def __init__(self, owner): + """@param owner: The NodeData instance that owns this manager.""" + self.head = None + self.tail = None + self.owner = owner + + def _assertLinkOwnership(self, link): + """This validates the passed-in link is actually owned by this + manager, i.e., the source of the link is the node this data is + attached to. Raises an exception if not.""" + if link.source is not self.owner.node: + raise WrongParentError("Link source is not the owner node.") + + def insertBefore(self, pos, link): + """Inserts the given link in front of pos. + + @param pos: The position to insert the node at. This can be a + link, in which case the new link will be inserted in front of + it, the value None in which case the link is placed on the end, + or a number which is interpreted as an index to put the new + link at. Note the latter should be a last resort, as it is the + slowest, triggering a linear crawl along the link list which + definately has an effect in practice.""" + + self._assertLinkOwnership(link) + + if type(pos) is types.IntType: + pos = self[pos] + + if pos is None: + # Update the link before and after. We can assume the link + # is currently detached, so we can do this freely. + currentTail = self.tail + link._prevLink = currentTail + link._nextLink = None # paranoia + if currentTail is None: + self.head = link + else: + currentTail._nextLink = link + self.tail = link + else: + # pos is a link + prevLink = pos._prevLink + nextLink = pos + link._prevLink = prevLink + link._nextLink = nextLink + pos._prevLink = link + if prevLink is None: + # new head + self.head = link + + def remove(self, link): + self._assertLinkOwnership(link) + linkPrev = link._prevLink + linkNext = link._nextLink + + if linkPrev is not None: + linkPrev._nextLink = linkNext + if linkNext is not None: + linkNext._prevLink = linkPrev + if link is self.head: + self.head = linkNext + if link is self.tail: + self.tail = linkPrev + + link._prevLink = None + link._nextLink = None + + def index(self, l): + """Returns the index of the given link. + + Raises an exception if the link doesn't belong to the + manager. + + @param l: The link to retrieve the index of.""" + try: + self._assertLinkOwnership(l) + except WrongParentError: + # convert to ValueError + raise ValueError("Link not in outgoing links.") + + for i, link in enumerate(self): + if link is l: + return i + raise ValueError("Link not in outgoing links.") + + def append(self, link): + """Appends the link to the tail.""" + self.insertBefore(None, link) + + def prepend(self, link): + """Prepends the link at the beginning.""" + self.insertBefore(self.head, link) + + def forwardIter(self, includeTerminalNone = False): + """This returns an iterator to move forward along the links. + + @param includeTerminalNone: A flag; if True, the terminal None + is included, which is useful internally. If False, only links + will be returned.""" + link = self.head + + while link is not None: + yield link + link = link._nextLink + + if includeTerminalNone: + yield None + + __iter__ = forwardIter + + def backwardIter(self, includeInitialNone = False): + """This returns an iterator to move backward along the links. + + @param includeInitialNone: If true, the first thing yielded + will be the None. If False, only Links will be yielded.""" + + if includeInitialNone: + yield none + + link = self.tail + while link is not None: + yield link + link = link._prevLink + + def __len__(self): + """Returns the number of links. Linear search; slow.""" + i = 0 + for _ in self: + i += 1 + return i + + def __getitem__(self, i): + """Returns the i'th link like Python lists do, or raise + ValueError. + + Note this requires a linear search and is slow if used + carelessly. However, used for small absolute values of i and + it can be the easiest way to retrieve the links that way.""" + #print "getitem in OutgoingLinkManager warning." + if i >= 0: + for count, link in enumerate(self.forwardIter()): + if count == i: + return link + raise IndexError("Link index out of range.") + else: + for count, link in enumerate(self.backwardIter()): + if -1 - count == i: + return link + raise IndexError("Link index out of range.") + + def __nonzero__(self): + return self.head is not None + + def __contains__(self, item): + for link in self.forwardIter(): + if link is item: + return True + return False Index: outline.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/outline.py,v retrieving revision 1.1 retrieving revision 1.2 diff -C2 -d -r1.1 -r1.2 *** outline.py 10 Mar 2005 04:25:57 -0000 1.1 --- outline.py 6 May 2005 03:58:04 -0000 1.2 *************** *** 16,20 **** __all__ = ["LinkHandle", "RootHandle", "Link", "BasicNode", ! "BasicDocument", "nodes", "Cursor", "quickMakeNodes"] # FIXME: How do I seperate "Document" from "What manipulations I want --- 16,20 ---- __all__ = ["LinkHandle", "RootHandle", "Link", "BasicNode", ! "BasicDocument", "Cursor", "quickMakeNodes", "EventPrinter"] # FIXME: How do I seperate "Document" from "What manipulations I want *************** *** 25,29 **** # Let's burn this bridge when we get to it. - # FIXME: node.data.outgoing -> node.outgoing # FIXME: handle.node.X -> handle.X # (need to double check that these things are non-overlapping) --- 25,28 ---- *************** *** 160,166 **** if self.creating: return "<Handle #%i under construction>" % self.id - if not hasattr(self, "id"): - import pdb - pdb.set_trace() return "<Handle #%i to link %s>" % (self.id, str(self.link)) --- 159,162 ---- *************** *** 380,395 **** "maximally dedented.") ! # Disconnect this link... ! self.link.source = None ! ! parentIndex = parentHandle.link.sourceIndex() # Inject the link into the grandparent and reconnect grandparentNode = grandparentHandle.getNode() grandparentData = grandparentNode.data ! grandparentData.outgoing.insert(parentIndex+1, self.link) ! self.link.source = grandparentNode ! # and this preserves the location of the link in the incoming ! # list of self.link.node . # Maintain handle information --- 376,385 ---- "maximally dedented.") ! parentLink = parentHandle.link # Inject the link into the grandparent and reconnect grandparentNode = grandparentHandle.getNode() grandparentData = grandparentNode.data ! self.link.setSource(grandparentNode, parentLink._nextLink) # Maintain handle information *************** *** 415,437 **** raise IndexError("Can't get a sibling of the root.") ! index = self.getIndex() ! if index is None: ! raise IndexError("Can't get a sibling because it seems " ! "this handle's parent doesn't know it has " ! "this handle's link as a child. (This is " ! "a bug.)" ) ! if index + delta < 0: ! raise IndexError("Can't get a sibling because the " ! "requested delta results in a " ! "negative index.") ! ! try: ! handle = parent[index + delta] ! except IndexError: ! raise IndexError("Can't get a sibling because there " ! "is no sibling corresponding to %s." ! % delta) ! return handle def deleteLink(self): --- 405,424 ---- raise IndexError("Can't get a sibling of the root.") ! # march backward or forward the given distance on the link ! # chain, then get that handle ! link = self.link ! newLink = link ! if delta < 0: ! incr, att = 1, "_prevLink" ! else: ! incr, att = -1, "_nextLink" ! while delta != 0: ! newLink = getattr(newLink, att) ! if newLink is None: ! raise IndexError("Can't get a sibling because it " ! "goes off the end of the list.") ! delta += incr ! return parent.getHandleForLink(newLink) def deleteLink(self): *************** *** 462,465 **** --- 449,453 ---- def notifyLinkEvent(self, event): """Pass events on, adding handle notation.""" + event = copy.copy(event) event.sourceHandle = self self.notifySubscribers(event) *************** *** 552,559 **** def notifyNodeEvent(self, event): """Pass events on, adding handle notation.""" event.sourceHandle = self self.notifySubscribers(event) - # FIXME: Automatically detach when source gets GC'ed? --- 540,547 ---- def notifyNodeEvent(self, event): """Pass events on, adding handle notation.""" + event = copy.copy(event) event.sourceHandle = self self.notifySubscribers(event) # FIXME: Automatically detach when source gets GC'ed? *************** *** 566,569 **** --- 554,561 ---- should do so, to preserve that information. + Links are also doubly-linked lists pointing back and forth to + their siblings, with None being the terminal pointer. This is used + mostly by Iron Lute internals. + Links subscribe to their dest node and will transmit any events from the source node to the link subscribers. *************** *** 588,591 **** --- 580,585 ---- self._dest = None self._source = None + self._prevLink = None + self._nextLink = None self.dest = dest self.source = source *************** *** 598,603 **** self.dest = None ! def attachSource(self): ! """Attaches the link to the source node in ._source.""" # This must never be called when the node is indeterminate --- 592,601 ---- self.dest = None ! def attachSource(self, pos = None): ! """Attaches the link to the source node in ._source. ! ! @param pos: An option position to attach the source in front ! of, as the link to attach in front of, None to append to the ! end, or an index of the link to attach in front of.""" # This must never be called when the node is indeterminate *************** *** 611,626 **** # If the link is already in the outgoing list, don't # add it; but do fire the event ! # FIXME: We really need a more explicit way of setting order ! # then the implicit way this provides ! if self not in source.data.outgoing: ! # Absent somebody manually inserting the node, put ! # it on the end. ! index = len(source.data.outgoing) ! source.data.outgoing.append(self) ! else: ! index = source.data.outgoing.index(self) event = subscriber.Event("child_added", source, ! node = self.dest, position = ! index) source.notifySubscribers(event) --- 609,616 ---- # If the link is already in the outgoing list, don't # add it; but do fire the event ! source.data.outgoing.insertBefore(pos, self) event = subscriber.Event("child_added", source, ! node = self.dest, ! prev = self._prevLink) source.notifySubscribers(event) *************** *** 663,670 **** dest.incoming.removeLink(self) ! def attach(self): ! """Attach both ends.""" self.attachDest() - self.attachSource() # If the dest has no document, set it to the source document. --- 653,668 ---- dest.incoming.removeLink(self) ! def attach(self, sourcePos = None): ! """Attach both ends. ! ! @private ! ! @param sourcePos: The source position, as either index, or the ! node to be inserted in front of. The sourcePos is allowed to ! be one past the end, if it is an index, to facilitate """ ! # The source is the more sensitive, since sourcePos can ! # be wrong, so make sure it runs correctly before attaching dest ! self.attachSource(sourcePos) self.attachDest() # If the dest has no document, set it to the source document. *************** *** 722,727 **** self.attach() ! def setSource(self, newSource): ! """Parallels setDest.""" # Assume newSource is different from oldSource --- 720,739 ---- self.attach() ! def setSource(self, newSource, pos = None): ! """Sets the source of the link. ! ! By default, or if used via the property, the node will be ! added to the end of the outgoing links, i.e., will be the last ! child. If you use the function call, you can set the pos. ! ! @param newSource: The node that the link comes from. ! @param pos: The pos of the new link, as either the link to ! insert the link in front of, or the position in the list to ! insert the link. (The former is highly preferred for ! performance reasons.) This can not be used to move a link ! around if the newSource is the same as the current source. ! The pos is allowed to be one past the end, which is equivalent ! to appending the node. Anything beyond that throws a value ! error.""" # Assume newSource is different from oldSource *************** *** 748,752 **** if not source: self._source = weakref.ref(newSource) ! self.attach() return --- 760,764 ---- if not source: self._source = weakref.ref(newSource) ! self.attach(pos) return *************** *** 777,780 **** --- 789,814 ---- source = property(getSource, setSource, delSource) + def moveUp(self): + """Tries to advance this link one entry up.""" + if self._prevLink is None: + raise ManipulationError("Can't move child up " + "past beginning of " + "children.") + prevLink = self._prevLink + source = self.source + self.source = None + self.setSource(source, prevLink) + + def moveDown(self): + """Tries to advance this link one entry down.""" + if self._nextLink is None: + raise ManipulationError("Can't move child down " + "past end of children.") + + nextLink = self._nextLink._nextLink + source = self.source + self.source = None + self.setSource(source, nextLink) + def notifyNodeEvent(self, event): """Adds the link annotation to the event and passes it on.""" *************** *** 972,977 **** --- 1006,1013 ---- def getChildLink(self, n): + #print "getChildLink n called, slow" return self.data.outgoing[n] def getChildLinkIndex(self, link): + #print "getChildLinkIndex called, slow" return self.data.outgoing.index(link) def getChildren(self): *************** *** 1001,1004 **** --- 1037,1045 ---- return len(self.data.outgoing) + def __iter__(self): + """Return each of the nodes that is a child of this node.""" + for link in self.data.outgoing: + yield link.dest + # FIXME as needed. def __deepcopy__(self, memo): *************** *** 1154,1186 **** return node - def swapChildren(self, aIndex, bIndex): - """Swaps the nodes indexed by aIndex and bIndex in the - children array.""" - - # It may seem easier to swap the dest nodes, but the problem - # is that would leave handles pointed at the wrong link->node. - # Thus, we need to actually switch at the source. - # also, - # out[aIndex], out[bIndex] = out[bIndex], out[aIndex] - # is not sufficient because it doesn't fire the right events - - if aIndex == bIndex: - return - - out = self.data.outgoing - if aIndex > len(out) or bIndex > len(out): - raise ValueError("Can't swap nodes because child indices " - "are out of range.") - if aIndex > bIndex: - aIndex, bIndex = bIndex, aIndex - firstLink = out[aIndex] - secondLink = out[bIndex] - firstLink.source = None - secondLink.source = None - self.data.outgoing.insert(aIndex, secondLink) - self.data.outgoing.insert(bIndex, firstLink) - secondLink.source = self - firstLink.source = self - def bases(cls): """Bases returns a list of base classes, starting with the --- 1195,1198 ---- |
From: Jeremy B. <th...@us...> - 2005-05-06 03:58:17
|
Update of /cvsroot/ironlute/ironlute/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv12730/tests Modified Files: outlineTest.py Log Message: Convert outgoing link list into a linked list, instead of an array. This makes getting the next or previous link a very efficient thing to do, which eliminates an accidental O(n^2) algorithm. Index: outlineTest.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/tests/outlineTest.py,v retrieving revision 1.1 retrieving revision 1.2 diff -C2 -d -r1.1 -r1.2 *** outlineTest.py 10 Mar 2005 04:26:01 -0000 1.1 --- outlineTest.py 6 May 2005 03:58:05 -0000 1.2 *************** *** 15,20 **** from outline import quickMakeNodes from constants import * import copy - import pdb class EventCatcher(unittest.TestCase): --- 15,20 ---- from outline import quickMakeNodes from constants import * + import ilerrors import copy class EventCatcher(unittest.TestCase): *************** *** 208,218 **** self.assert_(nodes.equivalentTo(unpickled)) ! def testSwapNodes(self): ! """Outline: Swap nodes works?""" self.clearEvents() before = quickMakeNodes(['a', 'b', 'c']) ! after = quickMakeNodes(['c', 'b', 'a']) before.subscribe(self) ! before.swapChildren(0, 2) # check that it functions --- 208,218 ---- self.assert_(nodes.equivalentTo(unpickled)) ! def testMoveUpDown(self): ! """Outline: Moving links up/down works?""" self.clearEvents() before = quickMakeNodes(['a', 'b', 'c']) ! after = quickMakeNodes(['b', 'a', 'c']) before.subscribe(self) ! before.data.outgoing[1].moveUp() # check that it functions *************** *** 221,239 **** # check that two child_added and two child_remove events # occurred. ! self.assert_(self.eventCount['child_added'] == 2) ! self.assert_(self.eventCount['child_removed'] == 2) self.clearEvents() ! # check that it doesn't error with the same arguments ! before.swapChildren(0, 0) ! self.assert_(before.equivalentTo(after)) ! # check that no events occurred at all ! self.assert_(len(self.eventCount) == 0) ! # Check overlarge arguments ! self.assertRaises(IndexError, before.swapChildren, 0, 3) ! self.assertRaises(IndexError, before.swapChildren, 3, 0) ! self.assert_(len(self.eventCount) == 0) # FIXME: Need to test in Handle def trestDeleteNode(self): --- 221,244 ---- # check that two child_added and two child_remove events # occurred. ! self.assert_(self.eventCount['child_added'] == 1) ! self.assert_(self.eventCount['child_removed'] == 1) self.clearEvents() ! self.assertRaises(ilerrors.ManipulationError, ! before.data.outgoing[0].moveUp) ! before = quickMakeNodes(['a', 'b', 'c']) ! after = quickMakeNodes(['a', 'c', 'b']) ! before.subscribe(self) ! before.data.outgoing[1].moveDown() ! ! self.assert_(before.equivalentTo(after)) ! self.assert_(self.eventCount['child_added'] == 1) ! self.assert_(self.eventCount['child_removed'] == 1) ! self.clearEvents() + self.assertRaises(ilerrors.ManipulationError, + before.data.outgoing[-1].moveDown) + # FIXME: Need to test in Handle def trestDeleteNode(self): *************** *** 263,267 **** self.assertRaises(ValueError, a.addChild, link = None, newChild = None) ! self.assertRaises(ValueError, a.addChild, newChild = b, pos = -33) a.addChild(b) --- 268,272 ---- self.assertRaises(ValueError, a.addChild, link = None, newChild = None) ! self.assertRaises(IndexError, a.addChild, newChild = b, pos = -33) a.addChild(b) *************** *** 382,386 **** d = outline.BasicNode() ! nodes.addChild(d, pos = 1) self.assert_(nodes.getChild(1) is d) self.assert_(len(d.incoming) == 1) --- 387,391 ---- d = outline.BasicNode() ! nodes.addChild(d) self.assert_(nodes.getChild(1) is d) self.assert_(len(d.incoming) == 1) *************** *** 407,418 **** if source is None: # Never "attached" to None ! return 0 try: source.data.outgoing.index(link) except ValueError: ! return 0 ! return 1 def linkDestAttached(link, dest): --- 412,423 ---- if source is None: # Never "attached" to None ! return False try: source.data.outgoing.index(link) except ValueError: ! return False ! return True def linkDestAttached(link, dest): |
From: Jeremy B. <th...@us...> - 2005-05-04 20:32:13
|
Update of /cvsroot/ironlute/ironlute/gtk In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv10018 Modified Files: IronLuteGtk.py gtkmenubar.py handletag.py outlinewidget.py Log Message: Menus displaying. Index: gtkmenubar.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/gtkmenubar.py,v retrieving revision 1.1 retrieving revision 1.2 diff -C2 -d -r1.1 -r1.2 *** gtkmenubar.py 27 Apr 2005 05:05:23 -0000 1.1 --- gtkmenubar.py 4 May 2005 20:32:03 -0000 1.2 *************** *** 10,22 **** guiSupport.IronLuteMenu.__init__(self, dispatcher) self.outlineWidget = outlineWidget def createMenus(self): for menu in self.menus: - print menu.name m = CommandMenu(menu, self.outlineWidget, menu.name) self.append(m) m.show() - class CommandMenu(gtk.MenuItem): """This implements a MenuItem that, when activated, rebuilds the --- 10,39 ---- guiSupport.IronLuteMenu.__init__(self, dispatcher) self.outlineWidget = outlineWidget + self.nodeMenu = None + self.documentMenu = None + + def checkActive(self): + """Updates the active status for the Node and Document + menus.""" + if self.nodeMenu is not None: + self.nodeMenu.cmds.checkActive(self.nodeMenu) + if self.documentMenu is not None: + self.documentMenu.cmds.checkActive(self.documentMenu) def createMenus(self): + """Creates the menus in the menubar. + + This is separated from initialization because the menu bar + needs to be created before the outline widget, + """ for menu in self.menus: m = CommandMenu(menu, self.outlineWidget, menu.name) + if menu.name == "Node": + self.nodeMenu = m + if menu.name == "Document": + self.documentMenu = m self.append(m) m.show() class CommandMenu(gtk.MenuItem): """This implements a MenuItem that, when activated, rebuilds the *************** *** 52,53 **** --- 69,97 ---- self.cmds.generateMenu(self, self.outlineWidget, context) + def ilDisable(self): + """Iron Lute telling this menu to disable itself.""" + self.set_sensitive(False) + + def ilEnable(self): + """Iron Lute telling this menu to enable itself.""" + self.set_sensitive(True) + + def ilAddSeparator(self): + """Add a separator to the menu.""" + item = gtk.SeparatorMenuItem() + item.show() + self.menu.append(item) + + def ilAddCommand(self, command, accelerator = None, accelString = + None, state = True): + """See command.py/CommandMenu.""" + + item = gtk.MenuItem(command.name) + # FIXME: Add a right-aligned label for the accelstring + item.set_sensitive(state) + item.show() + + def doCommand(): + self.outlineWidget.doCommand(command, self) + item.connect("activate", doCommand) + self.menu.append(item) Index: IronLuteGtk.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/IronLuteGtk.py,v retrieving revision 1.1 retrieving revision 1.2 diff -C2 -d -r1.1 -r1.2 *** IronLuteGtk.py 27 Apr 2005 05:05:23 -0000 1.1 --- IronLuteGtk.py 4 May 2005 20:32:03 -0000 1.2 *************** *** 51,63 **** self.vbox.show() - # Set up the menubar - # ---- - # Create the menubar - self.menubar = MenuBar(self, None) - self.menubar.createMenus() - self.menubar.show() - - self.vbox.add(self.menubar) - # Set up the outline widget self.sw = gtk.ScrolledWindow() --- 51,54 ---- *************** *** 69,72 **** --- 60,72 ---- self.outlineWidget.show() self.sw.show() + + # Create the menubar + self.menubar = MenuBar(self.outlineWidget, None) + self.outlineWidget.menubar = self.menubar + self.menubar.createMenus() + self.menubar.show() + self.outlineWidget.menubar.checkActive() + + self.vbox.add(self.menubar) self.vbox.add(self.sw) Index: outlinewidget.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/outlinewidget.py,v retrieving revision 1.1 retrieving revision 1.2 diff -C2 -d -r1.1 -r1.2 *** outlinewidget.py 27 Apr 2005 05:05:23 -0000 1.1 --- outlinewidget.py 4 May 2005 20:32:03 -0000 1.2 *************** *** 12,18 **** del d import guiSupport from handletag import HandleTag ! import constants # Note that TextViews, in order to scroll, must be placed in a --- 12,20 ---- del d + import outline import guiSupport from handletag import HandleTag ! import constants ! import ilerrors # Note that TextViews, in order to scroll, must be placed in a *************** *** 27,30 **** --- 29,33 ---- self.handleToTag = {} + self.gtkTagToHandleTag = {} self.nameToTag = {} self.rootHandle = rootHandle *************** *** 34,37 **** --- 37,42 ---- self.connect("move-cursor", self.cursorMove) + buffer = property(gtk.TextView.get_buffer) + def keypress(self, source, event): print event.keyval *************** *** 41,44 **** --- 46,51 ---- print "count", count print "sel", extendSelection + # We have to check the dynamic menus here + self.menubar.checkActive() def bindRootHandle(self): *************** *** 123,126 **** self.handleStates[handleToChange] = state ! buffer = property(gtk.TextView.get_buffer) --- 130,181 ---- self.handleStates[handleToChange] = state + def getInsert(self): + """Returns a GtkTextIter representing the cursor location.""" + cursorMark = self.buffer.get_insert() + return self.buffer.get_iter_at_mark(cursorMark) ! insert = property(getInsert) ! ! def getSelectionBound(self): ! """Returns a GtkTextIter representing the selection bound. ! ! The selection bound is the other end of the selection from the ! insert point; if they are equal, there is no selection.""" ! selMark = self.buffer.get_selection_bound() ! return self.buffer.get_iter_at_mark(selMark) ! ! selection_bound = property(getSelectionBound) ! ! def getCurrentHandleTag(self): ! """Returns the currently-focused HandleTag.""" ! cursor = self.insert ! tags = cursor.get_tags() ! ! # Scan the returned tags for something in ! # gtkTagToHandleTag ! for tag in tags: ! if tag in self.gtkTagToHandleTag: ! return self.gtkTagToHandleTag[tag] ! ! raise ilerrors.CantFindFocus("Tried to find the current " ! "focus, but failed.") ! ! currentHandleTag = property(getCurrentHandleTag) ! ! def getCurrentFocus(self): ! """Returns the currently-focused outline handle.""" ! return self.currentHandleTag.handle ! ! currentFocus = property(getCurrentFocus) ! ! def getSelection(self, sourceHandleTag = None): ! """Returns the current selection as an outline.Cursor.""" ! if sourceHandleTag is None: ! sourceHandleTag = self.currentHandleTag ! ! return outline.Cursor(sourceHandleTag.handle, ! self.insert.get_line_offset() - 1, ! self.selection_bound.get_line_offset() - 1) ! ! selection = property(getSelection) ! Index: handletag.py =================================================================== RCS file: /cvsroot/ironlute/ironlute/gtk/handletag.py,v retrieving revision 1.1 retrieving revision 1.2 diff -C2 -d -r1.1 -r1.2 *** handletag.py 27 Apr 2005 05:05:23 -0000 1.1 --- handletag.py 4 May 2005 20:32:03 -0000 1.2 *************** *** 38,41 **** --- 38,42 ---- # set margin self.tag = self.buffer.create_tag(str(self)) + self.outlineWidget.gtkTagToHandleTag[self.tag] = self textToInsert = handle.getNode().getData() *************** *** 108,111 **** --- 109,113 ---- Note using this invalidates GtkTextIters.""" + del self.outlineWidget.gtkTagToHandleTag[self.tag] begin = self.getMarkIter() # just after the indicator #begin.backward_char() # back up to get the indicator |
From: Jeremy B. <th...@us...> - 2005-04-27 05:05:33
|
Update of /cvsroot/ironlute/ironlute/gtk In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv22325 Added Files: IronLuteGtk.py gtkmenubar.py handletag.py indicator.py outlinewidget.py Log Message: Beginnings of a GTK interface. --- NEW FILE: indicator.py --- """Indicator is the little indicator to the left of the text nodes.""" import gtk import sys import os d = os.path.dirname(__file__) d = os.path.normpath(os.path.join(os.getcwd(), d)) fileDir = d if d not in sys.path: sys.path.append(d) d = os.path.normpath(os.path.join(d + os.pathsep, os.pardir)) if d not in sys.path: sys.path.append(d) del d from constants import * import cStringIO import re cursors = {NOCHILDREN: gtk.gdk.Cursor(gtk.gdk.X_CURSOR), OPEN: gtk.gdk.Cursor(gtk.gdk.BASED_ARROW_UP), CLOSED: gtk.gdk.Cursor(gtk.gdk.BASED_ARROW_DOWN)} class Indicator(gtk.EventBox): """Indicators accept a height and a width. The actual images used for indication are retrieved from the indicatorImage function, which generates a correctly-sized image from our source images and caches the results. """ def __init__(self, handleTag, handle, size): gtk.EventBox.__init__(self) self.handleTag = handleTag self.size = size self.handle = handle self.connect("button_press_event", self.clicked) self._state = None self.image = gtk.Image() self.add(self.image) self.image.show() self.connect_after("realize", self.setCursor) def setState(self, newState): if self._state == newState: return if not self.handle.getNode().hasChildren() and \ newState != NOCHILDREN: import pdb pdb.set_trace() raise ValueError("Setting a state other than NOCHILDREN " "on a handle with no children.") self._state = newState sourceImage = indicatorImage(newState, self.size, self.handle.getNode()) self.image.set_from_pixbuf(sourceImage) if self.window: self.setCursor() def setCursor(self, something = None): try: self.window.set_cursor(cursors[self._state]) except KeyError: # This can apparently still happen upon creation, and # I don't think this hurts as it still seems to get set pass def clicked(self, source, event): self.handleTag.toggle() # Stores the images used for the buttons images = {} # Stores the source from the disk so we don't constantly re-load it sourceImages = {} # Class names -> icon names iconNames = {} # Map the state to the state name stateToName = {OPEN: "children.open", CLOSED: "children.closed", NOCHILDREN: "nochildren"} iconDir = os.path.join(fileDir, "icons") def indicatorImage(state, size, node): """This function grabs the necessary icons for the node, sizes them as needed, and caches the results so they can be re-used. It returns a gtk.Image. @param state: The state of the node, OPEN, CLOSED, NOCHILDREN. @param size: The size of the necessary image, in pixels. @param node: The node we are requesting the icon for. The node's class' MRO will be examined, and the first .classId found in iconNames will use that name. Failing that, we'll use 'default'.""" # images are name with two parts, the Type of the node, and the # State of the node typeName = "default" for klass in node.__class__.__mro__: if hasattr(klass, "classId"): if klass.classId in iconNames: typeName = iconNames[klass.classId] break if (state, size, typeName) in images: return images[(state, size, typeName)] # Otherwise, we must generate it. stateName = stateToName[state] imageName = "%s.%s.png" % (typeName, stateName) try: pixbuf = sourceImages[imageName] except KeyError: pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(iconDir, imageName)) sourceImages[imageName] = pixbuf pixbuf = pixbuf.scale_simple(size, size, gtk.gdk.INTERP_BILINEAR) images[(state, size, typeName)] = pixbuf return pixbuf --- NEW FILE: IronLuteGtk.py --- import pygtk pygtk.require("2.0") import gtk import sys import os d = os.path.dirname(__file__) d = os.path.normpath(os.path.join(os.getcwd(), d)) if d not in sys.path: sys.path.append(d) d = os.path.normpath(os.path.join(d + os.pathsep, os.pardir)) if d not in sys.path: sys.path.append(d) del d import constants from outlinewidget import OutlineWidget from gtkmenubar import MenuBar import guiSupport from types import * # This is an init function; GTK already behaves as we want it # to, so we don't need it. (Unlike TK.) def GtkStartInit(): return def GtkFinishInit(): gtk.main() def GtkEndApp(*args, **kwargs): # FIXME: What insta-kills the app? gtk.main_quit() class IronLuteGtk(gtk.Window, guiSupport.IronLuteFrame): """IronLuteGtk corresponds to one visible window frame on the screen. This is the GTK specific part of that. IronLuteGtk binds to a DocumentView, which keeps track of the document and manages the saving and indication of the top.""" def __init__(self, initialView = None): guiSupport.IronLuteFrame.__init__(self, initialView) gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) # Set up the basic facts of the window self.connect("destroy", GtkEndApp) self.set_title("Iron Lute GTK Test") # The root vbox self.vbox = gtk.VBox() self.vbox.show() # Set up the menubar # ---- # Create the menubar self.menubar = MenuBar(self, None) self.menubar.createMenus() self.menubar.show() self.vbox.add(self.menubar) # Set up the outline widget self.sw = gtk.ScrolledWindow() self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.outlineWidget = OutlineWidget(self.initialView.topHandle) self.sw.add(self.outlineWidget) self.outlineWidget.show() self.sw.show() self.vbox.add(self.sw) self.vbox.show() self.add(self.vbox) self.show() import outline document = outline.quickMakeNodes(['sample text that ought to overflow' ' onto the next line', ['flute', 'moo', ['mem', 'me', 'ma', 'mo']], 'Cheesy poofs', 'tart and tangy']*20) rootHandle = document.getRootHandle() if __name__ == "__main__": GtkStartInit() myWidget = IronLuteGtk(rootHandle) GtkFinishInit() --- NEW FILE: outlinewidget.py --- import gtk import sys import os d = os.path.dirname(__file__) d = os.path.normpath(os.path.join(os.getcwd(), d)) if d not in sys.path: sys.path.append(d) d = os.path.normpath(os.path.join(d + os.pathsep, os.pardir)) if d not in sys.path: sys.path.append(d) del d import guiSupport from handletag import HandleTag import constants # Note that TextViews, in order to scroll, must be placed in a # ScrolledWindow. class OutlineWidget(gtk.TextView, guiSupport.IronLuteOutlineFrame): def __init__(self, rootHandle, **kw): guiSupport.IronLuteOutlineFrame.__init__(self, initialView = rootHandle) gtk.TextView.__init__(self) self.set_size_request(300, 200) self.set_wrap_mode(gtk.WRAP_WORD) self.handleToTag = {} self.nameToTag = {} self.rootHandle = rootHandle self.bindRootHandle() self.connect("key-press-event", self.keypress) self.connect("move-cursor", self.cursorMove) def keypress(self, source, event): print event.keyval def cursorMove(self, source, step, count, extendSelection, data = None): print "Step:", step print "count", count print "sel", extendSelection def bindRootHandle(self): buffer = self.buffer i = 0 for handle, _ in self.handleWalkerForward(self.rootHandle[0]): HandleTag(self, handle, buffer.get_end_iter(), i) i += 1 def setState(self, handleToChange, state): """Sets the expand/contract/nochildren state of a handle to the given state, managing the necessary GUI changes to update the visual state. Anybody can call this to update the state of a node. @param handleToChange: The handle corresponding to the node in the GUI to update. @param state: The state to set the node to; one of C{constants.NOCHILDREN}, C{constants.OPEN}, or C{constants.CLOSED}. @param force: Force the update to occur without short-circuiting. A paranoia parameter.""" # If we're already in the right state, exit handleState = self.handleStates[handleToChange] if handleState == state: return handleTag = self.handleToTag[handleToChange] handleTag.indicator.setState(state) # Now, we actually perform the manipulations to add or # subtract text if state == constants.OPEN: # Check the validity if not handleToChange.getNode().hasChildren(): raise InvalidTargetError("Can't expand node with " "no children.") self.handleStates[handleToChange] = constants.OPEN # If it's still not visible, no change; # this can happen if you are opening a handle # that is underneath a closed one if not self.isVisible(handleToChange): return lineNum = handleTag.getLineNumber() it = self.handleWalkerForward(handleToChange, True) # skip the first one, as it is handleToChange it.next() thisLine = lineNum + 1 for handle, depth in it: if handle in self.handleToTag: # Remove the old tag if it exists, paranoia self.handleToTag[handle].destroy() HandleTag(self, handle, self.buffer.get_iter_at_line(thisLine), thisLine) thisLine += 1 if state == constants.CLOSED: # check validity wasVisible = self.isVisible(handleToChange) if not handleToChange.getNode().hasChildren(): raise InvalidTargetError("Can't contract node with " "no children.") if wasVisible: import pdb #pdb.set_trace() it = self.handleWalkerForward(handleToChange, True) it.next() for handle, depth in it: self.handleToTag[handle].destroy() if handle in self.handleToTag: del self.handleToTag[handle] self.handleStates[handleToChange] = state buffer = property(gtk.TextView.get_buffer) --- NEW FILE: gtkmenubar.py --- """This is the menubar for Iron Lute, which can, technically, be stuck anywhere as long as it is bound to an OutlineFrame.""" import gtk import guiSupport class MenuBar(gtk.MenuBar, guiSupport.IronLuteMenu): def __init__(self, outlineWidget, dispatcher, *args, **kwargs): gtk.MenuBar.__init__(self, *args, **kwargs) guiSupport.IronLuteMenu.__init__(self, dispatcher) self.outlineWidget = outlineWidget def createMenus(self): for menu in self.menus: print menu.name m = CommandMenu(menu, self.outlineWidget, menu.name) self.append(m) m.show() class CommandMenu(gtk.MenuItem): """This implements a MenuItem that, when activated, rebuilds the constituent menu underneath it dynamically.""" def __init__(self, cmds, outlineWidget, *args, **kwargs): if 'dispatcher' in kwargs: self.dispatcher = kwargs['dispatcher'] del kwargs['dispatcher'] else: self.dispatcher = None gtk.MenuItem.__init__(self, *args, **kwargs) self.cmds = cmds self.outlineWidget = outlineWidget self.menu = gtk.Menu() self.set_submenu(self.menu) self.connect("select", self.updateMenu) def updateMenu(self, data = None): # Clear out the menus self.menu.foreach(lambda a: self.menu.remove(a)) curNode = self.outlineWidget.currentFocus.getNode() if curNode is None: context = [""] else: context = [x.classId for x in curNode.bases()] + [""] self.cmds.generateMenu(self, self.outlineWidget, context) --- NEW FILE: handletag.py --- """ handletag implements support for handling the tags associated with nodes. """ import gtk import pango import sys import os d = os.path.dirname(__file__) d = os.path.normpath(os.path.join(os.getcwd(), d)) if d not in sys.path: sys.path.append(d) d = os.path.normpath(os.path.join(d + os.pathsep, os.pardir)) if d not in sys.path: sys.path.append(d) del d import constants import itertools from indicator import Indicator tagNumbers = itertools.count().next class HandleTag(object): def __init__(self, outlineWidget, handle, myIter, lineNum): self.tagNumber = tagNumbers() self.handle = handle self.buffer = outlineWidget.buffer self.outlineWidget = outlineWidget self.outlineWidget.handleToTag[handle] = self self.outlineWidget.nameToTag[str(self)] = self indent = (handle.depth() - 1) * constants.INDENT_WIDTH # set margin self.tag = self.buffer.create_tag(str(self)) textToInsert = handle.getNode().getData() self.buffer.insert(myIter, textToInsert + "\n") self.tag.set_property("size-points", 15) size = self.tag.get_property("size") pixels = pango.PIXELS(size) self.tag.set_property("left-margin", indent) self.tag.set_property("indent", -pixels) # Set up the indicator anchorIter = self.buffer.get_iter_at_line(lineNum) self.anchor = self.buffer.create_child_anchor(anchorIter) self.indicator = Indicator(self, handle, pixels) self.indicator.show() self.outlineWidget.add_child_at_anchor(self.indicator, self.anchor) # Set the state up for the indicator if self.handle not in self.outlineWidget.handleStates: # If this handle is new, start it out as closed or # nochildren if self.handle.getNode().hasChildren(): self.outlineWidget.setState(self.handle, constants.CLOSED) else: self.outlineWidget.setState(self.handle, constants.NOCHILDREN) else: # Re-init, just to be sure self.outlineWidget.setState(self.handle, self.outlineWidget.handleStates[self.handle]) self.indicator.setState(self.outlineWidget.handleStates[self.handle]) self.buffer.apply_tag(self.tag, self.buffer.get_iter_at_line(lineNum), self.buffer.get_iter_at_line(lineNum + 1)) # Set a mark, which we use to retrieve line numbers and such lineIter = self.buffer.get_iter_at_line(lineNum) self.mark = self.buffer.create_mark(str(self) + "_mark", lineIter, True) def getState(self): """Retrieves the open/closed/nochildren state from the outline widget and returns it.""" return self.outlineWidget.handleStates[self.handle] def setState(self, state): """Sets the open/closed/nochildren state for the node, via calling .setState on the outlineWidget.""" self.outlineWidget.setState(self.handle, state) state = property(getState, setState) def toggle(self): """Toggles this node's expansion state, which just gets sent up to the outlinewidget with the appropriate handle passed up.""" nextState = {constants.OPEN: constants.CLOSED, constants.CLOSED: constants.OPEN, constants.NOCHILDREN: constants.NOCHILDREN} self.state = nextState[self.state] def destroy(self): """Destroy completely removes all of the text, the tag, and the indicator. Note using this invalidates GtkTextIters.""" begin = self.getMarkIter() # just after the indicator #begin.backward_char() # back up to get the indicator end = self.getMarkIter() end.forward_to_line_end() # end of line end.forward_char() # get the CRLF too self.buffer.delete(begin, end) self.outlineWidget.delete_mark(self.mark) def getMarkIter(self): return self.buffer.get_iter_at_mark(self.mark) def getLineNumber(self): """Returns the current line number of the handle.""" return self.getMarkIter().get_line() def bounds(self): """Returns an GtkTextIter covering from the beginning to the end of this widget. @param withIndicator If true, the range returned includes the indicator. Defaults to false. @returns TextIter, TextIter (tuple)""" return self.getMarkIter(), self.getMarkIter().forward_to_line_end() def clear(self): """Removes all text from this handle, leaving the indicator behind.""" self.buffer.delete(*self.bounds()) def getText(self): """Get this node as pure text.""" return self.buffer.get_text(*self.bounds()) def __str__(self): return "node_%s" % self.tagNumber |
From: Jeremy B. <th...@us...> - 2005-04-27 05:04:51
|
Update of /cvsroot/ironlute/ironlute/gtk In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv22073/gtk Log Message: Directory /cvsroot/ironlute/ironlute/gtk added to the repository |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:17
|
Update of /cvsroot/ironlute/ironlute/tk/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/tk/tests Added Files: generalTkTest.py lineWrapperTest.py tkTestBase.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: lineWrapperTest.py --- """Tk has a reasonably nice text widget, but it has one major flaw: It has this strange idea that if you have text that has the following lengths: 1 screen line 5 screen lines 1 screen line and you have the insertion cursor somewhere in the first line, to get to the bottom line, you need press down only twice. I hate this with a passion. It is lazy programming, not user focus. The good news is that "someday" this should be much more easily fixed; see http://www.talkaboutprogramming.com/group/comp.lang.tcl.announce/messages/3529.html (specifically, 'displaylines') which is supposedly implemented and accepted. As of this writing (May 2004), it's not in my actual copy of Tk or Tkinter, and with Gentoo I'm typically pretty up-to-date with these things. Therefore, I must assume that Iron Lute will be released long before everybody has updated to the Tk necessary to use 'displaylines'. Second, if the characters are not literally displayed on the screen at the time, Tk has no idea where they are. Thus, if you are on the top line of the screen and try to go up, or the bottom line and try to go down, you can't just add one line to the display coordinates and get the cursot position; IIRC the widget bails out and jumps to one of the endpoints. That means we get to try to compute the locations ourselves and pass Tk in the literal position that we want the cursor to be in (which it probably still doesn't understand but does correctly interpret at display time, which is good enough). That means we get to try to match the Tk line wrap algorithm, pixel for pixel (since even a single pixel may cause a line wrap). Oh, hooray; let's reverse-engineer a potentially platform-dependent, Tk version dependent, pixel-slinging algorithm with multiple fonts, God only knows what kerning, and lots of other tedious details. Anyhow, this is exactly the sort of thing that unit testing was made for; if a platform passes this testing suite then all is probably well. Also it looks pretty while it runs (since Tk requires actual screen updates to update its internal parameters.) """ # Next up: display our line breaks vs. tk line breaks in error # message, maybe w/ dlineinfo? import os import sys if __name__ != "__main__": f = __file__ d = os.path.normpath(os.path.join(os.getcwd(), os.path.dirname(f))) if d not in sys.path: sys.path.append(d) d = d[:d.rfind(os.sep)] if d not in sys.path: sys.path.append(d) d = d[:d.rfind(os.sep)] if d not in sys.path: sys.path.append(d) else: sys.path.append("..") sys.path.append("../..") import unittest import random import gui import outline import tk import tkTestBase from Tkinter import * # importing this directly results in some aliasing problems TextNodeWidget = tk.TextNodeWidget requestedLines = tk._requestedLines class LineWrapperTest(tkTestBase.TkTestBase): """This defines several tests to torture the line wrapping algorithm, both with extensive randomized testing and specific cases known to cause problems on various platforms.""" def setUp(self): tkTestBase.TkTestBase.setUp(self) self.hackWidgetForFullHeight() def getLines(self, text): """Wrap the requestedLines calls so we only have to pass the text in.""" return requestedLines(text, self.font, self.frame.height / self.frame.fontHeight, self.widget.width, debug = 1) def checkSomeText(self, text): """Given some text, validate Tk and requestedLines agree. This ought to be named "testSomeText", but then then Unit Test framework thinks this is a test.""" self.node.setData(text) self.il.update_idletasks() computedLines, computedWidth, lines = self.getLines(text) # Stupid TK won't give me the end, it just says None bbox = self.widget.bbox("1.end - 1char") tkLines = (bbox[1] / self.frame.fontHeight) + 1 if text: tkWidth = bbox[0] + self.font.measure(text[-1]) else: tkWidth = bbox[0] # Compute a comparision to display if this fails. compare = "We said:\n\n" compare += "|\n".join(lines) compare += "|\n" compare += "Tk said:\n\n" tkLineList = [self.widget.get("@0,%s" % (y * self.widget.fontHeight), "@65535,%s" % (y * self.widget.fontHeight)) for y in range(tkLines)] compare += "|\n".join(tkLineList) compare += "|\n\n" # I've added a 10-second wait upon failure so you have # a chance to see how Tk lays out the text. In rare cases, Tk actually # *cuts off* the last character, which you can see the text # window, and my wrapping algorithm is actually correct. # It only seems to happen if the last char is unicode # Thus, if Tk and my wrapping algorithm disagree by one line, # and my last line ends in unicode with no spaces in it, I # bypass the following to avoid what I consider a spurious # error. isUnicode = 0 try: lines[-1][-1].encode('ascii') except: isUnicode = 1 if tkLines == computedLines - 1 and \ (isUnicode or (len(lines[-1]) == 1 and len(lines[-1][0]) == 1)): print "aborted test case; see lineWrapperTest.py for explanation" return try: self.assert_(tkLines == computedLines, ("For text '%s', we computed a line count of " "%s, but Tk said %s (computed width %s, " "tk said %s).\n%s" % (text, computedLines, tkLines, computedWidth, tkWidth, compare)).encode('utf-8')) self.assert_(tkWidth == computedWidth, "For text '%s', we computed a line width of " "%s, but Tk said %s.\n%s" % (text, computedWidth, tkWidth, compare)) except: import time time.sleep(10) raise def testBasicLineWrapping(self): """Tk: Test line wrapping reverse engineering (basic)""" # Set up a node we can load text into self.checkSomeText('') self.checkSomeText(".") self.checkSomeText("This is a multi-word test.") self.node.setData("This is a multi-line test." * 4) self.checkSomeText("This is a multi-line test." * 4) def testSpacesTortureTest(self): """Tk: Test line wrapping w/ lots of spaces in the text""" # Unlike some text widgets, Tk won't collapse spaces at the # end of a line, it wraps them onto the next. Make sure # we correctly match that behavior. maxEms = self.maxEms() text = "M" * maxEms + " " * 10 self.checkSomeText(text) self.checkSomeText(text * 2) self.checkSomeText(text * 3 + "f") # Check large number of spaces in the middle text = "M" * (maxEms / 2) + " " * 60 + "M" * (maxEms / 2) self.checkSomeText(text) def testLineForceWrap(self): """Tk: Test line wrapping when word is longer then one line""" maxEms = self.maxEms() text = "M" * (maxEms + 1) self.checkSomeText(text) # Verify this was a forced wrap. self.assert_(self.getLines(text)[0] == 2) def testGeneralTortureTest(self): """Tk: Test Line Wrapping torture test""" # Basically, this throws everything we can think of # at the wrapper: Unicode, spaces, etc. dataChunks = ["MMMMMM ", "iiiiiiii ", "abcdef ", u" \u1234", u"ue\u1233\u1222", " M", "b b b b b b b b b", " " * 20, " kr ", "12345678 ", "90!@#$%^&*() ", "{}|:\"<>? "] widths = [self.font.measure(x) for x in dataChunks] maxWidth = max(widths) # Try to verify we never overflow the widget (with some # health leeway) maxUsableWidth = (self.widget.height / self.widget.fontHeight) \ * self.widget.width maxChoosable = maxUsableWidth / maxWidth # Fudge factor maxChoosable -= 20 for i in range(25): numberOfChoices = random.randint(10, maxChoosable) chunks = [] for i in range(numberOfChoices): chunks.append(random.choice(dataChunks)) self.checkSomeText(''.join(chunks)) def testUnicodeKerning(self): """Tk: Line wrapping: Unicode fonts aren't kerned.""" width = self.font.measure(u"\u1234") for i in range(10): self.assert_(self.font.measure(u"\u1234" * i) == i * width) if __name__ == "__main__": unittest.main() --- NEW FILE: tkTestBase.py --- """This provides a base class for GUI testing; for every test, it sets up a document, node, handle, il (Iron Lute frame instance), and font, and assigns those things to class attributes of that name. It also automatically shuts down the window at the end of the test. It is very useful for testing.""" import os import sys if __name__ != "__main__": f = __file__ if os.path.dirname(f) not in sys.path: sys.path.append(os.path.dirname(f)) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: sys.path.append("..") sys.path.append("../..") import unittest import gui import outline import tk from Tkinter import * class TkTestBase(unittest.TestCase): def setUp(self): gui.activateGui() self.document = outline.quickMakeNodes(['']) self.node = self.document[0] self.rootHandle = self.document.getRootHandle() self.handle = self.rootHandle[0] self.il = gui.IronLute(self.document) self.frame = self.il.frame self.il.update_idletasks() self.widget = self.frame.handlesToWidgets[self.handle] self.widget.focus_set() self.assert_(self.frame.currentFocus is self.handle) self.font = self.widget.font self.assert_(isinstance(self.widget, tk.textnodewidget.TextNodeWidget)) def tearDown(self): self.il.closeCheck() del self.document del self.node del self.rootHandle del self.handle del self.il del self.frame del self.widget del self.font def hackWidgetForFullHeight(self): """This hacks the widget to take up the full height of the frame, useful for testing the line wrapping algorithms.""" self.widget.place(height = self.frame.height) self.widget.height = self.frame.height def clearDocument(self, document = None): """Clear out the current document, and reset member appropriately.""" for link in self.document.data.outgoing[:]: link.clear() self.document.addNewChild() self.node = self.document[0] self.handle = self.rootHandle[0] # Should this be necessary # FIXME: handle should not be necessary self.frame.syncWithDocument(self.handle) self.il.update_idletasks() self.widget = self.frame.handlesToWidgets[self.handle] self.widget.focus_set() self.assert_(self.frame.currentFocus is self.handle) # ON POSTING EVENTS: # Ideally, I'd like to be able to say "The user pressed down." # Perhaps at some point we'll work out a way to do that with # platform-specific libraries. Unfortunately, postEvent("<Down>") # doesn't work as we'd like; it looks like Tk eats it. If we want # to check a keypress, we must manually call the event handler # method. def postEvent(self, event): """Posts an event to the window.""" self.il.winfo_toplevel().event_generate(event) self.il.update_idletasks() def maxEms(self, widget = None, char = "M", indent = 0): """Returns the maximum number of some char without wrapping. widget defaults to self.widget. char defaults to a capital M, since this function is named after the typographical unit. Another char can be substituted if necessary.""" if widget is None: widget = self.widget em = self.font.measure(char) return (self.widget.width - tk.outlineframe.constants.INDENT_WIDTH * indent) / em def maxLines(self): """Returns the maximum number of unclipped lines you can fit in the current frame with the current font.""" return self.frame.height / self.frame.fontHeight --- NEW FILE: generalTkTest.py --- """This performs a wide variety of general testing on the Tk interface. It tests that nodes resize, scrollbars are always correct, pressing 'up' or 'down' on the top or bottom of the screen does the sane thing, or other things of that nature.""" import os import sys if __name__ != "__main__": f = __file__ if os.path.dirname(f) not in sys.path: sys.path.append(os.path.dirname(f)) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: sys.path.append("..") sys.path.append("../..") import time from Tkinter import * import unittest import tkTestBase import tk #from outlineframe import LineSizeCache class EventMock(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) class GeneralTkTest(tkTestBase.TkTestBase): def trestDownGeneral(self): """Tk: Pushing 'down' works?""" # This is one of the foundational elements of the GUI, and # there's a lot of cases. # By my count, we have the following (mostly) independent axes: # * On the bottom line of the screen or not # * there's a node below you or not # * you're actually on the bottom line of your node or not # * the node below you is indented/same level/dedented # * current node extends off top / doesn't # That's 2 * 2 * 2 * 3 * 2 possibilities, or 48 distinct # possibilities. Wow. # FIXME: Also need to handle holding shift for a new selection, # holding shift to extend a selection, and holding shift # to wrap back on a selection (cursor at 4, selection 4-8, # after shift-down selection should be 8-12) # FIXME: Actually implement nodeExtendsOffTop # Forgive the non-standard indenting, reason should be # obvious: for bottomLineOfScreen in (True, False): for nodeBelow in (True, False): for bottomLineOfNode in (True, False): for indentLevel in (-1, 0, 1): for nodeExtendsOffTop in (True, False): self.doTestDown(bottomLineOfScreen, nodeBelow, bottomLineOfNode, indentLevel, nodeExtendsOffTop) def doTestDown(self, bottomLineOfScreen, nodeBelow, bottomLineOfNode, indentLevel, nodeExtendsOffTop): """Do the actual testing for this test case.""" # Print out which case we're running: #print ["not bottom line of screen, ", "bottom line of screen, " # ][bottomLineOfScreen], #print ["no node below, ", "node below, "][nodeBelow], #print ["not bottom of node, ", "bottom of node, " # ][bottomLineOfNode], #print ("indent level %s, " % indentLevel), #print ["node not extend off top", "node extends off top" # ][nodeExtendsOffTop] self.clearDocument() # Start constructing the test case. maxEms = self.maxEms() em = self.font.measure("M") oneLine = "M" * maxEms twoLines = oneLine + " " + oneLine maxLines = self.maxLines() indentWidth = tk.outlineframe.constants.INDENT_WIDTH # Make sure we have room to work with self.assert_(maxLines > 5) self.assert_(maxEms > 5) # FIXME: Ignoring nodeExtendsOffTop nodeToAddTo = self.document handle = self.rootHandle # We start out with one line on the screen. self.currentLines = 1 # If the indent = -1, we have to pull all these lines over # and redefine what "oneLine" and "twoLines" are if indentLevel == -1: handle = self.rootHandle[0] nodeToAddTo = handle.node maxEms = self.maxEms(indent = 1) oneLine = "M" * maxEms twoLines = oneLine + " " + oneLine # Start by padding the text node widget down to the # second-to-last line for i in range(maxLines - 3): nodeToAddTo.addNewChild(childArgs=(oneLine,)) self.frame.expandAll() # We now have two clear lines on the bottom. # What we do with them depends on "bottomLineOfNode" # and "bottomLineOfScreen" if bottomLineOfNode and bottomLineOfScreen: # A two-line widget that we're on the bottom of focusNode = nodeToAddTo.addNewChild(childArgs=(twoLines,)) widget = self.frame.handlesToWidgets[handle[-1]] widget.focus_set() widget.setCursorPos(maxEms + 4) elif bottomLineOfNode and not bottomLineOfScreen: # a one-line widget focusNode = nodeToAddTo.addNewChild(childArgs=(oneLine,)) widget = self.frame.handlesToWidgets[handle[-1]] widget.focus_set() widget.setCursorPos(4) elif not bottomLineOfNode and bottomLineOfScreen: # Another one-line widget to eat space, and then # a two-line widget that we are on the top line of nodeToAddTo.addNewChild(childArgs=(oneLine,)) focusNode = nodeToAddTo.addNewChild(childArgs=(twoLines,)) widget = self.frame.handlesToWidgets[handle[-1]] widget.focus_set() widget.setCursorPos(4) elif not bottomLineOfNode and not bottomLineOfScreen: # A two-line widget and we're one the first line focusNode = nodeToAddTo.addNewChild(childArgs=(twoLines,)) widget = self.frame.handlesToWidgets[handle[-1]] widget.focus_set() widget.setCursorPos(4) widget.update_idletasks() #print repr(widget.reflectedHandle.node), repr(focusNode) #print widget.reflectedHandle.node, ', ', focusNode self.assert_(focusNode is widget.reflectedHandle.node) originalX = tk.Bbox(widget.bbox(INSERT)).x originalTopHandle = self.frame.topHandle originalFocus = self.frame.currentFocus if nodeBelow: # Place a node below the current node with suitable # indentation. if indentLevel == -1: # Add to the original document to get a node # one to the left target = self.document.addNewChild(childArgs=(oneLine,)) elif indentLevel == 0: target = nodeToAddTo.addNewChild(childArgs=(oneLine,)) elif indentLevel == 1: target = focusNode.addNewChild(childArgs=(oneLine,)) self.frame.expandAll() self.frame.syncWithDocument() # We have now completely handled the construction for # everything except nodeExtendsOffTop # Make sure Tk is up-to-date, then actually run the Down # command self.frame.update_idletasks() noModifiers = EventMock(state = 0) widget.down(noModifiers) # Now, we start validating the results, based on the flags # If we were on the bottom and there was somewhere to go (a # node below the focused node, or another line in that node), # the outlineframe should have a different topHandle # Otherwise, it should be the same somewhereToGo = nodeBelow or not bottomLineOfNode if bottomLineOfScreen and somewhereToGo: self.assert_(self.frame.topHandle is not originalTopHandle) else: self.assert_(self.frame.topHandle is originalTopHandle) # If we were on the bottom line of the node and there was a # node below, see that the new focus is that new # node. Otherwise, the focus should be the same. if bottomLineOfNode and nodeBelow: self.assert_(self.frame.currentFocus.node is target) else: self.assert_(self.frame.currentFocus is originalFocus) self.assert_(self.frame.currentFocus in self.frame.handlesToWidgets) # Assert basic functioning of the XLock newX = tk.Bbox(self.frame.handlesToWidgets[self.frame.currentFocus].bbox(INSERT)).x # indentLevel * INDENT_WIDTH + newX - oldX should be within an # Em of zero # This is the difference between the 'pure' x the cursor # should have, and the real one it has indentDifference = indentLevel * indentWidth + newX - originalX # FIXME: Is this a legitimate problem? #print indentDifference, em, indentLevel, newX, originalX #self.assert_(-em <= indentDifference <= em) def trestUpGeneral(self): """Tk: Pushing 'up' works?""" # Mirror image to 'down' for topLineOfScreen in (True, False): for nodeAbove in (True, False): for topLineOfNode in (True, False): for indentLevel in (-1, 0, 1): for nodeExtendsOffBottom in (True, False): self.doTestUp(topLineOfScreen, nodeAbove, topLineOfNode, indentLevel, nodeExtendsOffBottom) def doTestUp(self, topLineOfScreen, nodeAbove, topLineOfNode, indentLevel, nodeExtendsOffBottom): """Do the actual testing for this test case.""" # Print out which case we're running: #print ["not top line of screen, ", "top line of screen, " # ][topLineOfScreen], #print ["no node above, ", "node above, "][nodeAbove], #print ["not top of node, ", "top of node, " # ][topLineOfNode], #print ("indent level %s, " % indentLevel), #print ["node not extend off bottom", "node extends off bottom" # ][nodeExtendsOffBottom] # Bypass some tests: If there's no node above, skip all but # one indentLevel if not nodeAbove and indentLevel != 0: return self.clearDocument() # Start constructing the test case. indentWidth = tk.outlineframe.constants.INDENT_WIDTH maxEms = self.maxEms() em = self.font.measure("M") oneLine = "M" * (maxEms - (indentWidth / em) - 1) twoLines = oneLine + " " + oneLine maxLines = self.maxLines() # Make sure we have room to work with self.assert_(maxLines > 5) self.assert_(maxEms > 5) # Business: # # * If nodeAbove, prepare and re-focus # * prepare for each of top(Node/Screen) target = self.handle self.node.setData(oneLine) # If there's a node above the target node, make it with the # proper indent if nodeAbove: if indentLevel == -1: # Make a child, that's our target target.node.addNewChild() target = target[0] elif indentLevel == 0: self.document.addNewChild() target = self.rootHandle[1] elif indentLevel == 1: # Complicated case target.node.addNewChild(childArgs=(oneLine,)) self.document.addNewChild() target = self.rootHandle[1] self.frame.expandAll() # We now have the node above set up; set up the target node # and screen position target.node.setData(twoLines) lineToSync = 0 if not topLineOfScreen: lineToSync -= 1 if not topLineOfNode: lineToSync += 1 self.frame.syncWithDocument(target, lineToSync) widget = self.frame.handlesToWidgets[target] if topLineOfNode: widget.setCursorPos(4) else: widget.setCursorPos(4 + maxEms) widget.update_idletasks() self.assert_(target is widget.reflectedHandle) originalX = tk.Bbox(widget.bbox(INSERT)).x originalTopHandle = self.frame.topHandle originalTopLine = self.frame.topLine originalFocus = self.frame.currentFocus self.frame.expandAll() self.frame.syncWithDocument() # We have now completely handled the construction for # everything except nodeExtendsOffTop # Make sure Tk is up-to-date, then actually run the Up # command self.frame.update_idletasks() noModifiers = EventMock(state = 0) #print "focus before: %s" % self.frame.currentFocus #print self.frame.handlesToWidgets #if not topLineOfNode: # time.sleep(5) self.assert_(self.frame.currentFocus in self.frame.handlesToWidgets) widget.up(noModifiers) #if not topLineOfNode: # time.sleep(5) # Now, we start validating the results, based on the flags # If we had to scroll, the topHandle or topLine should be # different if topLineOfScreen and nodeAbove: #print self.frame.topHandle, originalTopHandle, \ # self.frame.topLine, originalTopLine self.assert_((self.frame.topHandle is not originalTopHandle) or (self.frame.topLine != originalTopLine)) else: self.assert_(self.frame.topHandle is originalTopHandle) # If we were on the bottom line of the node and there was a # node below, see that the new focus is that new # node. Otherwise, the focus should be the same. if topLineOfNode and nodeAbove: if indentLevel == 1: self.assert_(self.frame.currentFocus is self.rootHandle[0][0]) else: self.assert_(self.frame.currentFocus is self.rootHandle[0]) else: self.assert_(self.frame.currentFocus is originalFocus) #print self.frame.currentFocus, self.frame.handlesToWidgets #print target #print list(self.rootHandle.depthFirst(yieldMarkers = 1)) self.assert_(self.frame.currentFocus in self.frame.handlesToWidgets) # Assert basic functioning of the XLock newX = tk.Bbox(self.frame.handlesToWidgets[self.frame.currentFocus].bbox(INSERT)).x # indentLevel * INDENT_WIDTH + newX - oldX should be within an # Em of zero # This is the difference between the 'pure' x the cursor # should have, and the real one it has indentDifference = indentLevel * indentWidth + newX - originalX # FIXME: Is this a legitimate problem? #print indentDifference, em, indentLevel, newX, originalX #self.assert_(-em <= indentDifference <= em) def testScrollbar(self): """Tk: Scrollbar works""" # The algorithm the scrollbar uses is generally recursive # so it *should* suffice just to verify the root handle's # linecount # Here's how to manually calculate a line count: def realLineCount(frame, handle): import outlineframe cache = outlineframe.LineSizeCache(frame) count = 0 for handle, depth in frame.handleWalkerForward(handle): count += frame.lines.handle[handle] return count def compare(): claimed = self.frame.lines.child[self.rootHandle] computed = realLineCount(self.frame, self.rootHandle[0]) self.assert_(claimed == computed, "Claimed line count %s does not match " "computed line count %s." % (claimed, computed)) compare() indentWidth = tk.outlineframe.constants.INDENT_WIDTH maxEms = self.maxEms() em = self.font.measure("M") oneLine = "M" * (maxEms - (indentWidth / em) - 1) eightLines = (oneLine + " ") * 8 self.node.setData(eightLines) compare() if __name__ == "__main__": unittest.main() |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:17
|
Update of /cvsroot/ironlute/ironlute/xmlmapping/demos/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/xmlmapping/demos/tests Added Files: opmlmappingTest.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: opmlmappingTest.py --- """This tests the OPML mapping demo.""" import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: if os.pardir not in sys.path: sys.path.append(os.pardir) if os.pardir + os.sep + os.pardir not in sys.path: sys.path.append(os.pardir + os.sep + os.pardir) if os.pardir + os.sep + os.pardir + os.sep + os.pardir not in sys.path: sys.path.append(os.pardir + os.sep + os.pardir + os.sep + os.pardir) import opmlmapping as opml om = opml.OPMLMapping test1 = """<opml version="1.0"> <head> <windowTop>34</windowTop> </head> <body> <outline text="this is a test data file" /> <outline text="this file has three children"> <outline text="and the second child has a child" /> </outline> <outline text="this is the third child" /> </body> </opml> """ class OPMLTester(unittest.TestCase): def testEqualityWorks(self): """XMLMapping Demo: OPML Equality works""" # Test equality to ensure further testing works correctly o1 = opml.OPMLDocument() o2 = opml.OPMLDocument() self.assert_(o1 == o2) # Check not headings o1.head['windowTop'] = 3 o2.head['windowTop'] = 2 self.assert_(not (o1 == o2)) # Check headings o1.head['windowTop'] = 2 o2.head['windowTop'] = 2 self.assert_(o1 == o2) # Check attributes o1.atts['version'] = 2 o2.atts['version'] = 1 self.assert_(not (o1 == o2)) # Check attributes o1.atts['version'] = 1 o2.atts['version'] = 1 self.assert_(o1 == o2) # Add a child on1 = opml.OPMLNode() on2 = opml.OPMLNode() o1.append(on1) self.assert_(not (o1 == o2)) o2.append(on2) self.assert_(o1 == o2) # Add an att to the children on1.atts['type'] = 'link' self.assert_(not (o1 == o2)) on2.atts['type'] = 'link' self.assert_(o1 == o2) # Add text to the children on1.text = 'me' self.assert_(not (o1 == o2)) on2.text = 'me' self.assert_(o1 == o2) def testSimple(self): """XMLMapping Demo: OPML mapper works""" opmlObj1 = om.textToObj(test1) opmlText = om.objToText(opmlObj1) opmlObj2 = om.textToObj(opmlText) # Assert everything survived self.assert_(opmlObj1 == opmlObj2) # Now we need to break it all down # Hang on... o = opmlObj2 self.assert_(o.atts['version'] == "1.0") self.assert_(o.head['windowTop'] == "34") self.assert_(o[0].text == "this is a test data file") if __name__ == "__main__": unittest.main() |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:16
|
Update of /cvsroot/ironlute/ironlute/tk In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/tk Added Files: IronLuteTk.py __init__.py bbox.py commandMenu.py dialogs.py handletag.py indicator.py outlineframe.py outlinewidget.py statusbar.py textnodewidget.py tkCommandMenu.py tkMenuBar.py tkclipboard.py tkposition.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: tkclipboard.py --- """Implements the Tk clipboard, with access to text only. May need to do some cross platform stuff. Windows may not be clipboardable without help.""" # FIXME: Make this a borg import os class TkClipboard: def __init__(self, master): self.master = master def setClipboard(self, s, obj = None): """Sets the clipboard to s, with option ref to obj. Sets the OS clipboard to the string s. If obj is attached, the clipboard will remember the obj. If the user requests the clipboard and the clipboard contents have not changed, TkClipboard will return obj instead of the string. This is how Iron Lute holds onto fancy node references in the clipboard.""" print repr(obj) if os.name == 'nt': self.master.clipboard_clear() self.master.clipboard_append(s) return if os.name == 'posix': self.clipboard = s self.master.selection_handle(self.handleSelection) self.master.selection_own(command=self.lostSelection) self.obj = obj def lostSelection(self, event = None): """Notices in X when the selection is lost.""" self.clipboard = None def handleSelection(self, offset, maxChars): """When another X application requests the clipboard, this gives it to them.""" return self.clipboard def getClipboard(self): """Returns the X selection, if any.""" if os.name == 'nt': try: contents = self.master.selection_get(selection="CLIPBOARD") if self.obj is not None and self.clipboard == \ contents: return self.obj else: return contents except: return "" if os.name == 'posix': try: contents = self.master.selection_get() print contents == self.clipboard if self.obj is not None and self.clipboard == \ contents: return self.obj else: return contents except: return "" --- NEW FILE: indicator.py --- """Indicator is the little indicator to the left of the text nodes.""" from Tkinter import * CLOSED = 0 OPEN = 1 CONFIG = 2 NOCHILDREN = 3 HIDDEN = 4 class Indicator(Canvas): """Indicators accept a height and a width. This default indicator uses no symbol for NONE, a closed black circle for CLOSED, closed green circle for CONFIG, and an open black circle for OPEN. Later we'll need to switch this to some sort of higher-level protocol that the node types can describe and the UI implement. """ def __init__(self, master, width, height, node, background = "white", foreground = "black", nochildren = "grey75", relief = FLAT, state = NOCHILDREN): Canvas.__init__(self, master = master, width = width, height = height, background = background, relief = relief, highlightthickness = 0, borderwidth = 0) self.state = None self.master = master self.node = node self.foreground = foreground self.background = background self.nochildren = nochildren self.width = width self.height = height self.object1 = None self.object2 = None self.setState(state) self.bind("<Button-1>", self.button1) self.bind("<Configure>", self.cancelEvent) def cancelEvent(self, event): """Cancels events we don't want bubbling up.""" return "break" def button1(self, event = None): self.node.button1(event) def setState(self, newState): if self.state == newState: return # no changes if self.object1 is not None: self.delete(self.object1) self.delete(self.object2) xcenter = self.width / 2 ycenter = self.height / 2 radius = min(xcenter, ycenter) / 2 # / 2 just looks nicer xmin = xcenter - radius xmax = xcenter + radius ymin = ycenter - radius ymax = ycenter + radius self.state = newState if newState == NOCHILDREN: self.object1 = self.create_arc((xmin, ymin, xmax, ymax), start = 0, extent = 180, fill = self.nochildren, outline = self.nochildren, style = CHORD) self.object2 = self.create_arc((xmin, ymin, xmax, ymax), start = 180, extent = 180, fill = self.nochildren, outline = self.nochildren, style = CHORD) if newState == CLOSED: self.object1 = self.create_arc((xmin, ymin, xmax, ymax), start = 0, extent = 180, fill = self.foreground, outline = self.foreground, style = CHORD) self.object2 = self.create_arc((xmin, ymin, xmax, ymax), start = 180, extent = 180, fill = self.foreground, outline = self.foreground, style = CHORD) if newState == OPEN: self.object1 = self.create_arc(xmin, ymin, xmax, ymax, start = 0, extent = 180, outline = self.foreground, style = ARC) self.object2 = self.create_arc(xmin, ymin, xmax, ymax, start = 180, extent = 180, outline = self.foreground, style = ARC) --- NEW FILE: bbox.py --- """bbox implements a class Bbox which wraps Tk bounding boxes so that I can access the components by name: .x, .y, .width, and .height.""" from types import * class Bbox: """Bbox wraps the Tk bounding box for my convenience, giving x, y, width, and height attributes.""" pos = {} pos["x"] = 0 pos["y"] = 1 pos["width"] = 2 pos["height"] = 3 def __init__(self, bbox = [0, 0, 0, 0]): try: if len(bbox) != 4: raise ValueError("Bounding boxes must be lists of four " "ints.") except TypeError: raise TypeError("Bounding box tried to be created from %r," " which is not a sequence." % bbox) for val in bbox: if type(val) != IntType: raise TypeError("Bounding boxes must be lists of four ints.") self.__dict__["bbox"] = list(bbox) def __getitem__(self, pos): return self.bbox[pos] def __setitem__(self, pos, value): self.__dict__["bbox"][pos] = value def __getattr__(self, name): if self.pos.has_key(name): return self.bbox[self.pos[name]] else: raise AttributeError("Attribute " + name + " not defined for bboxes.") def __setattr__(self, name, value): if type(value) != IntType: raise TypeError ( "Bboxes must recieve ints." ) if self.pos.has_key(name): self.__dict__["bbox"][self.pos[name]] = value else: raise AttributeError("Attribute " + name + " not defined for bboxes.") def __str__(self): return str(self.bbox) def __repr__(self): return str(self.bbox) def __hash__(self): return hash(self.bbox) --- NEW FILE: statusbar.py --- from __future__ import generators from Tkinter import Frame, Label, SUNKEN, W, X import Queue class StatusBar(Frame): """StatusBar implements a status bar that can have messages sent to it to be displayed, plus a duration for those messages to be displayed, unless pre-empted by later messages.""" def __init__(self, master, duration = 5): """Duration is the number of seconds the message will be displayed.""" Frame.__init__(self, master) self.label = Label(self, bd=1, relief=SUNKEN, anchor=W) self.label.pack(fill=X) self.duration = duration self.count = self.counter() self.current = None def set(self, format, *args): self.current = self.count.next() if len(args) == 0: self.label.config(text=format) else: self.label.config(text=format % args) self.label.update_idletasks() self.after(self.duration * 1000, self.clear, self.current) def clear(self, current): if current == self.current: self.label.config(text="") self.label.update_idletasks() def counter(self): i = 0 while 1: yield i i = i + 1 --- NEW FILE: tkCommandMenu.py --- """CommandMenu is a subclass of Menu, but instead of feeding it a series of commands, you feed it a command.CommandMenu and this menu automatically regenerates itself every time it is invoked, keeping up with its bound command.CommandMenu. Since it's a subclass of Menu, you can still create commands manually, but you need to do it in an override of updateMenu. Since the menu regenerates itself from scratch each time it's invoked, your changes will not hand around if only made in the constructor.""" from Tkinter import Menu, DISABLED, NORMAL import sys if ".." not in sys.path: sys.path.append("..") class CommandMenu(Menu): def __init__(self, cmds, outlineFrame, *args, **kwlist): # intercept keyword parameters we'd like if 'dispatcher' in kwlist: self.dispatcher = kwlist['dispatcher'] del kwlist['dispatcher'] else: self.dispatcher = None Menu.__init__(self, *args, **kwlist) self.cmds = cmds self.outlineFrame = outlineFrame self['postcommand'] = self.updateMenu def updateMenu(self): self.delete(0, 9999999) # just a big num to ensure we get everything curNode = self.outlineFrame.currentFocus.getNode() if curNode is None: context = [""] else: context = [x.classId for x in curNode.bases()] + [""] self.cmds.generateMenu(self, self.outlineFrame, context) def ilDisable(self): return def ilAddSeparator(self): self.add_separator() def ilAddCommand(self, command, accelerator = None, accelString = None, state = 1): """See command.py/CommandMenu.""" if accelerator is None: accelerator = "" else: accelerator = accelString if state: state = NORMAL else: state = DISABLED self.add_command(label=command.name, accelerator=accelerator, state=state, command=lambda c=command: self.outlineFrame.doCommand(c, self)) #for command in iter(self.cmds): # self.add_command(label=command.name, # command=lambda c=command: self.outlineFrame.doCommand(c)) # FIXME: This ought to be done by the 'command history', # whatever that will be. --- NEW FILE: outlineframe.py --- """OutlineFrame is the core of Iron Lute. This can be included in other things as needed. OutlineFrame manages the TextNodeWidgets underneath it, manages the scollbar, and managing command dispatching. """ from __future__ import generators from Tkinter import * import math import weakref from textnodewidget import TextNodeWidget, requestedLines from tkposition import TkPosition from bbox import Bbox import outline import constants from ilerrors import * import dispatcher [...1032 lines suppressed...] currentFocus = property(getFocus, setFocus, delFocus, "The currently-focused outline node.") # FIXME: Del this # Helps track down topNode setting errors if any #def getTopNode(self): # return self.__topNode #def setTopNode(self, node): # import traceback # self.__topNode = node #topNode = property(getTopNode, setTopNode) if __name__ == "__main__": root = Tk() import outline d = outline.BasicDocument() outlineframe = OutlineFrame(d, parent = root) outlineframe.pack() root.mainloop() --- NEW FILE: outlinewidget.py --- """ Outline widget manages the handle, hoisting, etc. TextNodes implement tag operations on the text, very similar to the current TextNodeWidget, only the former outline frame is now a text node, and the text nodes are now just objects. It is too late to try to implement this, but if I can carve a day out I bet I could crank out a prototype, in which case I'd be nearly done with the dev release! """ from Tkinter import * import tkFont import indicator import handletag import sys import os d = os.path.dirname(__file__) d = os.path.normpath(os.path.join(os.getcwd(), d)) if d not in sys.path: sys.path.append(d) d = os.path.normpath(os.path.join(d + os.pathsep, os.pardir)) if d not in sys.path: sys.path.append(d) del d import guiSupport import outline document = outline.quickMakeNodes(['sample text', ['flute', 'moo']]) rootHandle = document.getRootHandle() class OutlineWidget(Text, guiSupport.IronLuteOutlineFrame): def __init__(self, master, font = None): guiSupport.IronLuteOutlineFrame.__init__(self) self.master = master self.destroyed = 0 if font is None: try: self.font = tkFont.Font ( family="Georgia", size="15" ) except: self.font = tkFont.Font ( family="Times", size="14" ) else: self.font = font Text.__init__(self, master, wrap = WORD, undo = 0, font = self.font) self["highlightcolor"] = self["highlightbackground"] self["background"] = "white" self["padx"], self["pady"] = (0, 0) self["highlightthickness"] = 0 self["borderwidth"] = 0 self.scrollbar = Scrollbar(master, command = self.yview) self.scrollbar.pack(side = RIGHT, fill = Y) self["yscrollcommand"] = self.scrollbar.set self.commandExecuting = 0 self.fontMetrics = self.font.metrics() self.fontHeight = self.fontMetrics["linespace"] self.myTag = "myTag" # I want tags to "eat" chars, even if they are added just # before the tag starts, so I actually have to start the # tags at "%s.end" % (line - 1). To understand this better, # take out the elide hack here and try to add to an outline # at the very first position. self.insert("1.0", "\n") self.tag_add("elidehack", "1.0", "2.0") self.tag_config("elidehack", elide = 1) self.handleToTag = {} self.nameToTag = {} handle = rootHandle[0] self.handleToTag[handle] = handletag.HandleTag(self, handle, 2) handle = rootHandle[1] self.handleToTag[handle] = handletag.HandleTag(self, handle, 3) handle = handle[0] self.handleToTag[handle] = handletag.HandleTag(self, handle, 4) self.bind("<Key>", self.key) self.bind("<KeyRelease>", self.keyRelease) # Use tag_names(index) to determine what node is being used # and set Enter accordingly. def key(self, event): # Prevent people typing left of the insert marker if self.index(INSERT)[-2:] == ".0": self.mark_set(INSERT, INSERT + " +1char") return "break" def keyRelease(self, event): print self.index(INSERT) print self.tag_names(self.index(INSERT)) if __name__ == "__main__": root = Tk() myWidget = OutlineWidget(root) myWidget.pack() root.mainloop() --- NEW FILE: __init__.py --- """Imports all the tk stuff in.""" from Tkinter import * import tkFont from tkposition import TkPosition from bbox import Bbox from statusbar import StatusBar from textnodewidget import TextNodeWidget, _requestedLines from outlineframe import OutlineFrame from IronLuteTk import IronLuteTk, TkStartInit, TkFinishInit from dialogs import * from tkclipboard import TkClipboard from tkMenuBar import MenuBar --- NEW FILE: handletag.py --- """ handletag implements support for handling the tags associated with nodes, and the children of all nodes. """ import indicator import sys import Tkinter import os d = os.path.dirname(__file__) d = os.path.normpath(os.path.join(os.getcwd(), d)) if d not in sys.path: sys.path.append(d) d = os.path.normpath(os.path.join(d + os.pathsep, os.pardir)) if d not in sys.path: sys.path.append(d) del d import constants def tagNumbers(): i = 1 while 1: yield i i = i + 1 tagNumbers = tagNumbers() i = 0 class HandleTag(object): def __init__(self, outlineWidget, handle, lineNum): global i self.tagNumber = tagNumbers.next() self.outlineWidget = outlineWidget self.outlineWidget.handleToTag[handle] = self self.outlineWidget.nameToTag[str(self)] = self # Set up the indicater indent = (handle.depth() - 1) * constants.INDENT_WIDTH self.config(lmargin1 = 0 + indent, lmargin2 = outlineWidget.fontHeight + indent) # Make a line for ourselves outlineWidget.insert("%s.0" % lineNum, "\n") self.indicator = indicator.Indicator(outlineWidget, outlineWidget.fontHeight, outlineWidget.fontHeight, handle) outlineWidget.window_create("%s.0" % lineNum, align = Tkinter.CENTER, window = self.indicator) outlineWidget.tag_add(self, "%s.0" % lineNum, "%s.0" % (lineNum + 1)) self.handle = None self.hasText = False self.bindHandle(handle) outlineWidget.tag_raise(self) def getRange(self): ranges = self.outlineWidget.tag_ranges(self) if len(ranges) > 2: # FIXME: log print "More than one range for %s! (%s)" \ % (self, ranges) return ranges def bindHandle(self, handle): if self.handle is handle: return if self.hasText: # Clear out any text this might have, as a paranoia thing # FIXME: Check read only begin, end = self.getRange() self.outlineWidget.delete(begin + "+1char", end) # FIXME: factor begin, end = self.getRange() print begin self.outlineWidget.insert(begin + "+1char", handle.node.getData()) def cancelEvent(event): return "break" self.bind("<Enter>", cancelEvent) self.handle = handle def get(self): return self.outlineWidget.get(*self.getRange())[:-1] def bind(self, *args, **kwds): return self.outlineWidget.tag_bind(self, *args, **kwds) def config(self, **kwds): return self.outlineWidget.tag_config(self, **kwds) def __str__(self): return "node_%s" % self.tagNumber def __hash__(self): return self.tagNumber --- NEW FILE: tkposition.py --- from types import StringType, IntType; class TkPosition: """Wraps Tk positions for IronLute. Not generally useful. This class is a convenience class that takes in positions, allows sensible Python operations on it, and outputs positions. Since my app only has one line per widget, I assume that we're always on line 1 when I manipulate positions. (In particular, for decrementing purposes.""" def __init__(self, pos): self.line, self.col = self.convPos(pos) def convPos(self, pos): "Returns a tuple (row, col) from a Tk string." if "." not in str(pos): return (1, int(pos)) pos = str(pos) return (int(pos[:pos.find(".")]), int(pos[pos.find(".")+1:])) def pos(self): "Returns position in tuple form." return (self.line, self.col) def __repr__(self): return "%d.%d" % (self.line, self.col) def __str__(self): return "%d.%d" % (self.line, self.col) def __int__(self): return self.col def __cmp__(self, other): if type(self) != type(other) and type(other) != StringType: raise NotImplementedError("TkPositions can't be compared to anything but" " other TkPositions or strings.") s = self.pos() if type(self) == type(other): o = other.pos() else: o = self.convPos(other) if s[0] < o[0]: return -1 if s[0] > o[0]: return 1 return cmp(s[1], o[1]) def __hash__(self): return hash(str(self)) def __add__(self, i): if type(i) != IntType: raise TypeError("Can only add ints to TkPositions.") col = self.col + i if col < 0: col = 0 return TkPosition("%d.%d" % (self.line, col) ) def __sub__(self, i): if type(i) != IntType: raise TypeError("Can only subtract ints from TkPositions.") col = self.col - i if col < 0: col = 0 return TkPosition("%d.%d" % (self.line, col) ) --- NEW FILE: textnodewidget.py --- import sys if ".." not in sys.path: sys.path.append("..") from Tkinter import * from tkposition import TkPosition import tkCommandMenu from bbox import Bbox from indicator import * import outline from constants import * # These keys are passed through to the text widget directly and invoke # the processing keysToPassThrough = "abcdefghijklmnopqrstuvwxyz1234567890-=[]\;',./`" \ "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+{}|:\"<>?~ " # Conversion dict, to take Tk keysyms and convert them to Dispatcher # key ids. [...1163 lines suppressed...] widthCounter += element if not firstWord: widthCounter += paddingWidth if debug: if not firstWord: lines[-1].append(padding) lines[-1].append(data[i]) if lineCounter >= maxLines: return maxLines, widthCounter if debug: return lineCounter, widthCounter, \ [''.join(x) for x in lines] return lineCounter, widthCounter # Redo requested lines for easier testing: # text, font, maxLines, maxWidth # return the lineCounter and the final width for comparision against # Tk's opinion. maxLines may be set to None --- NEW FILE: commandMenu.py --- """CommandMenu is a subclass of Menu, but instead of feeding it a series of commands, you feed it a command.CommandMenu and this menu automatically regenerates itself every time it is invoked, keeping up with its bound command.CommandMenu. Since it's a subclass of Menu, you can still create commands manually, but you need to do it in an override of updateMenu. Since the menu regenerates itself from scratch each time it's invoked, your changes will not hand around if only made in the constructor.""" from Tkinter import Menu class CommandMenu(Menu): def __init__(self, cmds, outlineFrame, *args, **kwlist): Menu.__init__(self, *args, **kwlist) self.cmds = cmds self.outlineFrame = outlineFrame self['postcommand'] = self.updateMenu def updateMenu(self): self.delete(0, 9999999) # just a big num to ensure we get everything for command in iter(self.cmds): self.add_command(label=command.name, command=command.classId) # FIXME: This ought to be done by the 'command history', # whatever that will be. --- NEW FILE: tkMenuBar.py --- """This is the menu for IronLute, which can be stuck anywhere.""" from Tkinter import * import guiSupport import tkCommandMenu class MenuBar(Menu, guiSupport.IronLuteMenu): """The Iron Lute menu bar.""" def __init__(self, outlineframe, dispatcher, *args, **kwargs): Menu.__init__(self, *args, **kwargs) guiSupport.IronLuteMenu.__init__(self, dispatcher) self.frame = outlineframe self.tkMenus = [] def createMenus(self): for menu in self.menus: m = tkCommandMenu.CommandMenu(menu, self.frame) #m['font'] = menufont self.add_cascade(label=menu.name, menu=m) self.tkMenus.append(m) def setFocus(self, nodeFocus, docFocus): if nodeFocus is None: self.entryconfigure(5, state=DISABLED) else: self.entryconfigure(5, state=NORMAL) if docFocus is None: self.entryconfigure(6, state=DISABLED) else: self.entryconfigure(6, state=NORMAL) --- NEW FILE: dialogs.py --- """Iron Lute does a lot of its own work, but there are some dialogs that must be supplied by the GUI toolkit. This file implements them for Tk. Contents: * FileSelectionDialog: Selects a file from the local filesystem. * simpleFileOpener: Takes in a simpleFile class and returns a simpleFile object to the user's specification. """ import FileDialog import os import sys if __name__ != "__main__": # add parent to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) from tkSimpleDialog import Dialog from Tkinter import * import textnodewidget class FileSelectionDialog(FileDialog.FileDialog): """All implementations of FileSelectionDialog must have this interface: d = FileSelectionDialog(outlineFrame) filename = d.getFileSelection(dirToStartIn) and filename must be "None" (for anything that results in a file not being returned, most likely a cancel event), or a string containing the name of the selected file. (Only one file may be returned.) If your widget toolkit has no use for the outlineFrame reference, then just ignore it.""" def getFileSelection(self, startDir): return self.go(startDir) # FIXME: Ought to work in some validation code. class SimpleFileOpener(Dialog): """SimpleFileOpener provides a dialog box that takes in a simpleFile class type, and returns a created simpleFile object of that class. It uses the introspection capabilities of the simpleFile to do so.""" def __init__(self, master, simpleFileClass): self.simpleFileClass = simpleFileClass self.entries = [] Dialog.__init__(self, master, "Open New %s..." % simpleFileClass.name) def body(self, master): for param in self.simpleFileClass.paramList: paramName, paramDesc = param[:2] Label(master, text=paramName + ":").grid(row = i) self.entries.append(Entry(master)) self.entries[-1].grid(row = i, column = 1) return self.entries[0] def apply(self): params = [x.get() for x in self.entries] self.result = self.simpleFileClass(*params) class YesNoDialog(Dialog): """Asks a yes/no question.""" def __init__(self, master, question, summary = "", yesText = "Yes", noText = "No"): self.question = question self.yesText = yesText self.noText = noText if summary != "": summary = ": " + summary Dialog.__init__(self, master, "Yes/No" + summary) def body(self, master): t = "\n".join(wrap(self.question, 60)) Label(master, text=t).pack() box = Frame(master) w1 = Button(box, text = self.yesText, width = 10, command = self.yes, default = ACTIVE) w1.pack(side = LEFT, padx = 5, pady = 5) w2 = Button(box, text = self.noText, width = 10, command = self.no) w2.pack(side = LEFT, padx = 5, pady = 5) box.pack() return w1 def yes(self, event = None): self.answer = 1 self.exit() def no(self, event = None): self.answer = 0 self.exit() def exit(self): self.withdraw() self.update_idletasks() self.destroy() def buttonbox(self): return class tkGetKey(Dialog): """Get a keypress for a keyboard shortcut. Get the final result from tkGetKey.result, which will either have None for a cancelled dialog box or unacceptable input, or a string suitable for Dispatcher use.""" def __init__(self, master, name, *args, **kwargs): self.name = name self.shiftVar = IntVar() self.altVar = IntVar() self.controlVar = IntVar() self.result = None Dialog.__init__(self, master, "Select Keyboard Shortcut") def shift(self): return self.shiftVar.get() def alt(self): return self.altVar.get() def control(self): return self.controlVar.get() def body(self, master): self.boxLabel = Label(master, text="Select a " 'keyboard shortcut for\n"%s"' % self.name) self.boxLabel.grid(row = 0, column = 0, columnspan = 3) self.keyLabel = Label(master, text="Key:") self.keyLabel.grid(row = 1, column = 0, sticky = E) self.text = Text(master, width = 7, height = 1) self.text.grid(row = 1, column = 1, columnspan = 2, sticky = W) self.text.bind("<Key>", self.keyEvent) self.text.bind("<ButtonRelease-2>", self.cancelEvent) self.shift = Checkbutton(master, text="Shift", variable = self.shiftVar, command = self.updateKeypress) self.shift.grid(row = 2, column = 0) self.alt = Checkbutton(master, text="Alt", variable = self.altVar, command = self.updateKeypress) self.alt.grid(row = 2, column = 1) self.control = Checkbutton(master, text = "Control", variable = self.controlVar, command = self.updateKeypress) self.control.grid(row = 2, column = 2) return self.text def keyEvent(self, event): if event.keysym in textnodewidget.symsToIgnore: return if event.keysym in textnodewidget.keysymConvert: self.key = textnodewidget.keysymConvert[event.keysym] elif len(event.keysym) > 1: self.key = event.keysym else: self.key = event.char self.updateKeypress() return "break" def updateKeypress(self): try: self.text.delete("1.0", END) prefix = "" if self.shiftVar.get(): prefix += "S" if self.altVar.get(): prefix += "A" if self.controlVar.get(): prefix += "C" if prefix != "": prefix += "-" self.text.insert("1.0", prefix + self.key) except: pass def cancelEvent(self, event = None): return "break" def apply(self): """Processes the dialog box and returns a string suitable for the dispatcher.""" self.result = self.text.get("1.0", END) self.result = self.result.replace("\n", "") if self.result == "": self.result = None class ListChoice(Dialog): """List choice provides a simple drop-down list choice, and returns the selected object or objects in a list.""" def __init__(self, master, choices, caption = "Choice", text = "Choose one of the following:", multi = 0): """Choices is either a list of string choices, which will be directly returned by this class, or a list of [string, obj] choices, with the user choosing the string and this class returning the obj associated with that string directly to the class user. The list need not be homogenous.""" self.caption = caption self.choices = choices self.text = text self.multi = multi self.result = None Dialog.__init__(self, master) def body(self, master): l = Label(master, text = self.text) l.pack() if self.multi: lbtype = EXTENDED else: lbtype = SINGLE frame = Frame(master) scrollbar = Scrollbar(frame, orient=VERTICAL) lb = self.lb = Listbox(master, selectmode = lbtype, exportselection = 0, yscrollcommand = scrollbar.set) scrollbar.config(command=lb.yview) lb.pack(side = LEFT, fill = BOTH, expand = 1) scrollbar.pack(side = RIGHT, fill = Y, expand = 1) frame.pack() for choice in self.choices: try: caption = choice[0] except: caption = str(choice) lb.insert(END, caption) lb.pack() def apply(self): sel = [int(x) for x in self.lb.curselection()] result = [] for index in sel: try: result.append(self.choices[index][1]) except: result.append(self.choices[index]) self.result = result # This is a little oversophisticated for the use I'm putting it to, # but it's imported from another project so hopefully (cross my # fingers) it's well tested... if not, I want to know. ;-) import re # Not some of my best work. Then again, first pass attempts to clean # this up have failed to have any significant impact. It's just an # ugly algorithm with special cases sticking out everywhere. def wrap(s, width, maxlinecount = None, reserve = None): """Wraps a string in a fairly sophisticated way. s is the string that should be wrapped. width is the maximum allowable width. maxlinecount, if given, is the maximum number of lines that will be returned. reserve is the space reserved on the last line, if the string would end up going over the maxlinecount. If the maxlinecount is not exceeded, a list of strings, one for each line, is returned. If the maxlinecount is exceeded, a tuple is returned, where the first element is the remaining (unwrapped) string, and the second element is the list of wrapped strings. """ s = re.sub("\s+", " ", s) s = s.split(" ") lines = [] counter = 0 lastLine = 0 currentLine = "" if maxlinecount == 1: lastLine = 1 # Start making lines. for w in range(len(s)): word = s[w] l = len(word) if lastLine and reserve is not None: maxwidth = width - reserve else: maxwidth = width if counter + l < maxwidth and currentLine == "": currentLine = s[w] counter += l elif counter + l + 1 < maxwidth and currentLine != "": currentLine += " " + word counter += l + 1 else: # Spilling over limit, take action if maxlinecount == None: # always keep going if currentLine != "": lines.append(currentLine) currentLine = word while len(currentLine) > maxwidth: lines.append(currentLine[0:maxwidth]) currentLine = currentLine[maxwidth:] counter = len(currentLine) else: if currentLine != "": lines.append(currentLine) currentLine = word while len(currentLine) > maxwidth and len(lines) < maxlinecount: if len(lines) == maxlinecount - 1 and reserve is not None: maxwidth = width - reserve lines.append(word[0:maxwidth]) s[w] = s[w][maxwidth:] word = s[w] currentLine = word counter = len(currentLine) if len(lines) == maxlinecount: # hit the limit return (" ".join(s[w:]), lines) if len(lines) == maxlinecount - 1: lastLine = 1 if currentLine != "": lines.append(currentLine) return lines # Tuples: (input, output) testcases = [] testcases.append((("Hello, my name is Belvedere.", 10), ["Hello, my", "name is", "Belvedere."])) testcases.append((("longnameisnowhere,allcowerinfaceofthelongword", 10, 2, 5), ('re,allcowerinfaceofthelongword', ['longnameis', 'nowhe']))) testcases.append((("longnameisnowhere,allcowerinfaceofthelongword", 10, 2), ('lcowerinfaceofthelongword', ['longnameis', 'nowhere,al']))) def test(): for test in testcases: if apply(wrap, test[0]) != test[1]: raise ImportError("Error on wrap input in case: " + str(test[0]) +\ ", got " + str(apply(wrap, test[0])) + " instead" +\ " of " + str(test[1])) return # Ensure that wrap tests itself every time test() --- NEW FILE: IronLuteTk.py --- """IronLuteTk is responsible for knitting the various pieces of Iron Lute into a coherent application. It tracks open windows and terminates the program when they all close, and do other things that turn the outlineframe widget into a real application.""" from Tkinter import * import tkFont from statusbar import StatusBar from outlineframe import OutlineFrame from tkclipboard import TkClipboard import tkCommandMenu import tkMenuBar from types import * import guiSupport _a = None # This is a chance to do whatever configuration we want before # starting def TkStartInit(): global _a _a = Tk() try: # try to use Tile to make things prettier _a.tk.call('package', 'require', 'tile') _a.tk.call('namespace', 'import', '-force', 'ttk::*') for theme in ['xpnative', 'winnative', 'aqua', 'alt']: try: _a.tk.call("tile::setTheme", theme) break except: pass except: # no tile pass _a.withdraw() def TkFinishInit(): _a.mainloop() def TkEndApp(): _a.destroy() class IronLuteTk(Frame, guiSupport.IronLuteFrame): """IronLuteTk corresponds to one visible window frame on the screen. This is the Tk specific part of that. IronLuteTk binds to a DocumentView, which keeps track of the document and manages the saving and indicating of the top. """ def __init__(self, initialView = None): guiSupport.IronLuteFrame.__init__(self, initialView) width = 400 height = 350 master = Toplevel(width = width, height = height) self.clipboard = TkClipboard(self) self.master = master Frame.__init__(self, master, width=width, height=height, background="white") self.statusBar = StatusBar(self) self.setStatusBar(" ") reqheight = self.statusBar.winfo_reqheight() self.statusBar.reqheight = reqheight # self.documents tracks all documents included in this # window... maybe I should re-map that to being the # responsibilty of document objects them selves? # Note: Draw responsiblilities for who has to keep track of # what documents contain what documents, make sure a frame can # track all sub documents. I guess maybe it DOES belong in the # document classes, so I can ask for all children documents, # easily note deletions across all frames, and correct ask for # the "binding handle" in a document. self.resizing = 0 try: font = tkFont.Font ( family="Utopia", size="13" ) #font = tkFont.Font ( family="Times", size="10" ) menufont = tkFont.Font(family="Lucida", size="12") except: font = tkFont.Font ( family="Times", size="14" ) menufont = tkFont.Font(family="Times", size="12") self.frame = OutlineFrame(width, height - reqheight, parent = self, font = font, statusBar = self.statusBar, titleSetter = self) # Menu details self.menu = tkMenuBar.MenuBar(self.frame, self.dispatcher) self.frame.menu = self.menu master.config(menu = self.menu) # Create a temp menu command so we get the right size) self.menu.add_command(label=" ",command = lambda: 0) self.update_idletasks() reqheight += self.menu.winfo_reqheight() self.menu.delete(1) #self.frame.pack() if self.menu is not None: self.menu.createMenus() # Events self.master.bind("<Configure>", self.configure) self.statusBar.bind("<Configure>", lambda s: "break") self.bind("<Configure>", lambda s: "break") self.master.protocol("WM_DELETE_WINDOW", self.closeCheck) self.master.title(self.initialView.displayedDocument.title + " - Iron Lute") self.pack() self.configure((width, height)) def setStatusBar(self, text): self.statusBar.set(text) def setTitle(self, title): self.master.title(title) def terminateWindow(self): """Destroy this window's widget, if necessary.""" self.resizing = 1 # Don't worry about the configure event, # we're dying! (prevent exceptions) self.destroy() self.master.withdraw() self.master.destroy() def terminateApp(self): """End the entire application.""" _a.quit() def configure(self, eventOrSizeTuple): # handles window resizing # Re-arranging the widgets internally re-fires the configure # event, so we have to lock it down while we're re-arranging # the window. if type(eventOrSizeTuple) == TupleType: width, height = eventOrSizeTuple else: width = eventOrSizeTuple.width height = eventOrSizeTuple.height if not self.resizing: self.blockConfigEvent(1) reqheight = self.statusBar.reqheight self.frame.place(x = 0, y = 0, width = width, height = height - reqheight) self.frame.setWidthHeight(width, height - reqheight) self.statusBar.place(x = 0, y = height - reqheight, width = width) #self.frame.scrollbar.place(x = width - reqwidth, y = 0, height = # height - reqheight) self.place(x = 0, y = 0, width = width, height = height) self.master.update_idletasks() self.blockConfigEvent(0) def blockConfigEvent(self, val): self.resizing = val |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:16
|
Update of /cvsroot/ironlute/ironlute/xmlmapping/demos In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/xmlmapping/demos Added Files: README opmlmapping.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: README --- This directory houses some demos of the XMLMapper library. While these are demos, the are also perfectly usable. In complexity order, the demos are: OPML ---- This implements a simple bi-directional OPML transform. "Simple" is a relative term here; it's too simple for Iron Lute but is otherwise useful. See opml.py for documentation on how to use the object model. The OPML transform is the simplest actually useful demo here. It demonstrates: * Container mapping. * "Greedy" attributes. --- NEW FILE: opmlmapping.py --- """This file implements a simple two-way OPML transform. See the OPML spec at http://www.opml.org/spec for more information about the OPML spec. OPML documents are represented by the OPMLDocument object. An OPML document has OPMLNodes. If you want a more sophisticated transform, it's probably best just to copy this file and make your changes; this is intended as a demo, not really usable code. Note that the OPML files output by at least one of the major OPML generators, Radio Userland, can be illegal XML. This will only parse legal XML.""" from __future__ import generators import os import sys if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) if f == os.sep: sys.path.append("..") else: sys.path.append("..") import xmlmapping import mapper import attribute class OPMLDocument(list): """An OPML document has a dict named 'head' that has name->value pairs corresponding to the tags in the head element of the OPML document. It's your responsibility to keep the keys XML legal. All attributes are strings. This is subclassed from list for convenience and nodes are stored in this list.""" def __init__(self): self.head = {} self.atts = {} def __repr__(self): return "OPMLDocument at %s, %s children, header: %s, atts: %s" % \ (id(self), len(self), self.head, self.atts) def __eq__(self, other): # for the testing if type(other) is not type(self): return 0 return list.__eq__(self, other) and \ self.head == other.head and \ self.atts == other.atts class OPMLNode(list): """An OPMLNode has a .value, along with a .atts dictionary.""" def __init__(self, value = ""): self.text = "" self.atts = {} def __repr__(self): return "Node: %s, atts: %s, children: %s" % \ (self.text, self.atts, len(self)) def __eq__(self, other): if type(self) is not type(other): return 0 return list.__eq__(self, other) and \ self.text == other.text and \ self.atts == other.atts # The OPML spec is defined at http://www.opml.org/spec . # Now lets define the mapping: m = xmlmapping.XMLMapping() # The <opml> tag defines an <opml> document... let's # build it up. # An OPML document contains two distinct parts, a <head> and a <body>, # so we're looking for a type of MultipartMapper. class OPMLDocMapper(mapper.MultipartMapper): # We can specify a few things in this class since it's so narrowly # focused: def __init__(self, xmlMapper, tagName = None, klass = None, attributes = None, direction = None): # We can feed the MultipartMapper the partMappers, since we # know what we're doing head = xmlmapping.MapperDefinition(mapper.SimpleDictMapper) head.update(tagName = 'head', klass = dict) body = xmlmapping.MapperDefinition(mapper.ContainerMapper) # 'passthrough' because I'm defining the presentObject here body.update(tagName = 'body', klass = OPMLDocument, passthrough = 1) mapper.MultipartMapper.__init__(self, xmlMapper, tagName = tagName, klass = klass, direction = direction, partMappers = [[head, lambda o: o.head], [body, lambda o: o]], attributes = attributes) self.container = None # While parsing an OPML document, we get either a dict from the # <head> element, or OPMLNodes. def presentObject(self, obj): if self.container is None: self.container = OPMLDocument() if type(obj) == dict: self.container.head = obj else: # assume it's an OPMLNode self.container.append(obj) # Of course we really should have some error checking here, # right? Though truthfully, the framework catches a lot. def endMapper(self): mapper.MultipartMapper.endMapper(self) self.attsToObj(self.container, self.xmlAttributes) self.xmlMapper.propogateFinishedObject(self.container) # Define the OPML document mapper: opmlMapper = xmlmapping.MapperDefinition(OPMLDocMapper) opmlMapper.update(klass = OPMLDocument, tagName = 'opml') # It takes the attributes on the XML tag and stores them in # '.atts', and has a default version of "1.0" if none is given opmlMapper.addAtt(attribute.DictAtt('atts', xmlDefaults = {'version': '1.0'}, objDefaults = {'version': '1.0'})) # Now add the mapper we just described to the XMLMapping m.registerMapper(opmlMapper) # Finally, we need to define the children. Each <outline> tag is # itself a container that may contain one or more children. It has a # "text" attribute that we want to map to the .text field of the # object, and may contain arbitrary other attributes on the XML. # Note for our purposes we only pay special attention to "text"; in # theory you can also pay special attention to the other defined # elements like "isComment", but that's pretty straightforward. outlineMapper = xmlmapping.MapperDefinition(mapper.ContainerMapper) outlineMapper.update(klass = OPMLNode, tagName = "outline") # Here, we pay special attention to "text" but throw everything else # into a dict. Extending it to pay special attention to other things # should be obvious. outlineMapper.addAtt(attribute.StringAtt('text')) outlineMapper.addAtt(attribute.DictAtt('atts')) m.registerMapper(outlineMapper) # <outline> tags are contained in <body> tags # the <body> does nothing, really, except indicate that outline nodes # are coming up # That's it; we've defined the relevant parts. # Give "m" a better name now that we're through OPMLMapping = m del m |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:15
|
Update of /cvsroot/ironlute/ironlute/xmlmapping In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/xmlmapping Added Files: README __init__.py attribute.py mapper.py names.py xmlmapping.py xmlmappingconstants.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: attribute.py --- """An Attribute is used by the tag mapper class and probably by others. This file defines the Attribute interface and some of the simpler attributes. If an attribute has been 'consumed' by an attribute handler, it should remove the attribute from the passed 'atts' dict in attToObj. This allows chaining of the attributes meaningfully, allowing a 'catch-all' mapper at the end to do something meaningful without double-processing something. (Unless you *want* it to...)""" from xmlmappingconstants import * # FIXME: Docs need work as this class continues to evolve. class Attribute(object): """An 'interface' for the Attribute class. These can actually be used to collect either attributes on an object, or parameters in a dict for later instantiation. Attributes not existing in one form will not be assigned in the other. The proper way to think of this is not necessarily as a one-to-one mapping of attribute-to-object, but as a thing that can handle one conceptual attribute. For instance, in the OPML mapping, where nodes can have arbitrary attributes, a single Attribute subclass handles that case.""" def __init__(self, xmlDefaults = None, objDefaults = None): self.xmlDefaults = xmlDefaults self.objDefaults = objDefaults def objToAtt(self, obj, attDict): raise NotImplementedError() def attToObj(self, obj, atts): raise NotImplementedError() # Services provided to the attributes: Default XML and default Obj # dicts. # These are here so they can be easily replaced later. Right now # it just takes dicts and copies in the relevant keys. def setXmlDefaults(self, attDict): """Loads the xmldefaults from self.xmlDefaults.""" xmlDefaults = self.xmlDefaults if xmlDefaults is not None: for key, item in xmlDefaults.iteritems(): if key not in attDict: attDict[key] = item def setObjDefaults(self, d): objDefaults = self.objDefaults if objDefaults is not None: for key, item in objDefaults.iteritems(): if key not in d: d[key] = item class StringAtt(Attribute): """This slaps the string from the XML into the instanceAtt, and calls 'str' on the instanceAtt to get the XML.""" def __init__(self, xmlAttName, instanceAttName = None): Attribute.__init__(self) self.xmlAttName = xmlAttName if instanceAttName is not None: self.instanceAttName = instanceAttName else: self.instanceAttName = xmlAttName def objToAtt(self, obj, attDict): if hasattr(obj, self.instanceAttName): xmlAttName = self.xmlAttName attDict[xmlAttName] = str(getattr(obj, self.instanceAttName)) def attToObj(self, obj, atts): """Gets all the attributes because this could be a composite value or something.""" if self.xmlAttName in atts: setattr(obj, self.instanceAttName, atts[self.xmlAttName]) del atts[self.xmlAttName] class DictAtt(Attribute): """A DictAtt represents a mapping between some dictionary of attributes and xml attributes on a tag. This is common for XML tags that allow arbitrary attributes, like in OPML.""" # FIXME: Namespace support? def __init__(self, dictName, inFunc = None, outFunc = None, xmlDefaults = None, objDefaults = None): """dictName is the name of the dict on the object we are converting. To map the entire object, use '__dict__' for the dict name and provide a good inFunc and outFunc. inFunc will be used when converting from xml into the object. If given, it will be passed every key, value from the XML and must return a key, value to put into the dict. outFunc is the same, only it will be used on the way out, taking every key, value from the object's dict and converting to a string or unicode for each element. By default, the converter will use 'unicode(key), unicode(value)'. xmlDefaults is a dict that are attributes that will be added to the XML if there is no corresponding attribute already existing; this is useful for things like required version labels or other things your object model may not have. objDefaults is the same, only it contains keys that will be added to the object if the key does not already exist in the obj dict. Both functions may return None as the key to skip processing that attribute, but otherwise *must* return a str or unicode value; there is no exception handling for those functions in this class so you must handle all exceptions or you'll hose the entire conversion process.""" Attribute.__init__(self, xmlDefaults, objDefaults) self.dictName = dictName self.inFunc = inFunc self.outFunc = outFunc def objToAtt(self, obj, attDict): if not hasattr(obj, self.dictName): self.setXmlDefaults(attDict) return d = getattr(obj, self.dictName) # The following code is factored out this way for performance, # to avoid the 'if' on every loop # Use provided function.... if self.outFunc is not None: outFunc = self.outFunc for key, value in d.iteritems(): key, value = outFunc(key, value) if key is not None: attDict[key] = value # or use the default else: for key, value in d.iteritems(): attDict[unicode(key)] = unicode(value) # Load in the xmlDefaults, if any self.setXmlDefaults(attDict) def attToObj(self, obj, atts): if not hasattr(obj, self.dictName): setattr(obj, self.dictName, {}) d = getattr(obj, self.dictName) # Use provided function... if self.inFunc is not None: inFunc = self.inFunc for key, value in atts.iteritems(): key, value = inFunc(key, value) if key is not None: d[key] = value # ... or use the default else: for key, value in atts.iteritems(): d[key] = value # Load in objDefaults, if any self.setObjDefaults(d) --- NEW FILE: mapper.py --- """This file defines the mapper interface and some of the simpler Mapper objects.""" import sys import streams from streams import xmlobjects as xmlo, ExpatXMLComponent from xmlmappingconstants import * import xmlmapping # What is going on? if "names" in sys.modules: names = sys.modules["names"] else: import names class Mapper(object): """A Mapper is responsible for some discrete aspect of the translation between an object and an XML stream.""" # Mappers are created solely through keyword arguments, # except the xmlMapper at the beginning. def __init__(self, xmlMapper, submappers = None, attributes = None, direction = None): # submappers are mappers that will be pushed onto the # xmlmapping if direction is None: raise XmlMapperError("Mapper object can't be created " "because no direction was given.") self.direction = direction self.xmlMapper = xmlMapper self.pushedMappers = [] if attributes is None: self.attributes = [] else: self.attributes = attributes if submappers is None: return for submapper in submappers: self.pushMapper(submapper) def objToXML(self, obj, output): """This method is overridden to do whatever the mapper needs to do to the obj to output it. Note this may include calls back to the XmlMapper itself. 'output' is a function that expects one argument, the result to spit out into the stream.""" raise NotImplementedError def XMLToObj(self, xmlObject): """Receives the next xmlObject in the stream, allowing the Mapper to handle it appropriately.""" # Stores the atts from the xmlObject for potential later use raise NotImplementedError def endMapper(self): """endMapper is called when the tag this mapper is responsible for has finally terminated. It's a good time to call propogateObject in subclasses. Be sure to call this in those subclasses, to remove any pushed submappers.""" for mapper in self.pushedMappers: self.xmlMapper.popMapper(mapper) def pushMapper(self, mapperDef): """This passes through to the pushComponent method on the _XMLMapper this is attached through. Use this to remember what you've pushed on and automatically pop them when this mapper is done.""" self.xmlMapper.pushMapper(mapperDef) self.pushedMappers.append(mapperDef) def willMap(cls, xmlMapping, startElement): """This is a classmethod that will be offered an _XMLMapping object (for context) and an xmlobject representing a start element. This method returns true if this mapping is willing to accept this tag type, and false otherwise. By default, this will return false.""" return 0 willMap = classmethod(willMap) def presentObject(self, obj): """This method is called to present an object to this mapper on the stack, coming from a deeper tag, and ask it if it wants to handle this object. Return a true value to 'eat' this object, return false to pass it through. It is permissible to change the object or do other things with it without eating it. Default method ignores it and returns 0.""" return 0 # The following are really just services presented to subclasses, # rather then "methods" in the traditional sense. def objToAttsDict(self, obj): """Takes the attributes defined in the mapper and returns a dict appropriate for passing to the XML StartElement object.""" atts = {} try: for att in self.attributes: att.objToAtt(obj, atts) return atts # ignore if self.attribute doesn't exist except AttributeError: pass # ignore if it's not iterable (i.e., None) except TypeError: pass return atts # FIXME FIXME FIXME: "xmlToObj" and "XMLToObj"? WTF!?! def xmlToObj(self, obj, xmlObject): try: self.attsToObj(obj, xmlObject.attributes) # don't care if xmlObject.attributes doesn't exist except AttributeError: pass def attsToObj(self, obj, atts): """Given the attribute dictionary, process the object. (May be called in endMapper.)""" try: if atts is not None: for att in self.attributes: att.attToObj(obj, atts) # don't care if self.attributes doesn't exist except AttributeError: pass # Used by a lot of children def setTagName(self, tagName = None): if isinstance(tagName, names.Name): self.tagName = tagName self.namespace = tagName.namespace else: self.tagName = names.nameFromString(tagName) self.namespace = self.tagName.namespace class DummyMapper(Mapper): """Dummy Mapper ignore the elements it is assigned to.""" def __init__(self, xmlMapper, tagName = None, klass = None, submappers = None, direction = None): Mapper.__init__(self, xmlMapper, submappers = submappers, direction = direction) def XMLToObj(self, xmlObject): pass def objToXML(self, obj, output): pass class TagMapper(Mapper): """A TagMapper converts a given object into a simple tag and back again.""" def __init__(self, xmlMapper, klassArgs = (), klassDict = None, klass = None, tagName = None, attributes = None, submappers = None, direction = None): # Attributes is a list of attributes, as described below """tagName is the name of the tag to output, klass is a Python class to instantiate when this tag is encountered, with the given args and dict.""" Mapper.__init__(self, xmlMapper, attributes = attributes, submappers = submappers, direction = direction) # Sanity check: # This component needs a fully bi-directional class<=>tagName # mapping if klass is None or tagName is None: if klass is None: raise TypeError("Can't instantiate TagMapper without " "a class to create.") else: raise TypeError("Can't instantiate TagMapper without " "a tag to create.") self.tagName = tagName self.klass = klass self.klassArgs = klassArgs if klassDict is None: self.klassDict = {} else: self.klassDict = klassDict self.received = [] def objToXML(self, obj, output): """Outputs the <tagName></tagName> into the stream.""" atts = self.objToAttsDict(obj) output(xmlo.StartElement(self.tagName, atts)) output(xmlo.EndElement(self.tagName)) def XMLToObj(self, xmlObject): if isinstance(xmlObject, xmlo.StartElement): if xmlObject.name != self.tagName: raise CantHandleStreamError(\ "Can't handle '%s' begin tag in TagMapper for " "'%s'." % (xmlObject.name, self.tagName)) if len(self.received) != 0: # Already received this tag, can't nest them with this # mapper. raise CantHandleStreamError(\ "Can't nest '%s' elements in '%s' elements w/ " "TagMapper; TagMapper is too stupid." % (self.tagName, self.tagName)) # Received proper start element self.received.append(xmlObject) # Constuct the object self.obj = self.klass(*self.klassArgs, **self.klassDict) self.xmlToObj(self.obj, xmlObject) # Can't get here if this isn't right elif isinstance(xmlObject, xmlo.EndElement): pass # in other words, don't raise the following exception else: raise CantHandleStreamError(\ "Can't handle anything after <%s> except </%s> in " "TagMapper." % self.tagName) def endMapper(self): """Return the obj we constructed earlier (due to the simplicity of this mapper this method is easy).""" self.xmlMapper.propogateFinishedObject(self.obj) Mapper.endMapper(self) class ContainerMapper(Mapper): """A ContainerMapper maps an XML object into some container class. ContainerMapper requires a function that returns an iterator when passed the member of the class to be converted to a stream. By default it will simply call 'iter' on the object. Several common iterators are provided(?) This default ContainerMapper throws all children away. Override presentObject to do what you want with the children.""" def __init__(self, xmlMapper, klassArgs = (), klassDict = None, klass = list, tagName = None, attributes = None, getContained = iter, submappers = None, passthrough = 0, direction = None): # Attributes is a list of attributes as in TagMapper """tagName is the name of the tag to output, klass is a Python class to instantiate when this tag is encountered, with the given args and dict. passthrough allow the container mapper to passthrough the contained objects without processing; see opml.py demo.""" Mapper.__init__(self, xmlMapper, attributes = attributes, submappers = submappers, direction = direction) # Sanity check: This component needs a fully bi-directional # class<=>tagName mapping if klass is None or tagName is None: if klass is None: raise TypeError("Can't instantiate ContainerMapper " "without a class to create.") else: raise TypeError("Can't instantiate ContainerMapper " "without a class to create.") self.getContained = getContained self.tagName = tagName self.klass = klass self.klassArgs = klassArgs if klassDict is None: self.klassDict = {} else: self.klassDict = klassDict # Tracks if we've seen the open tag for this container self.started = 0 # We care about the children generated... self.passthrough = passthrough if direction == XML2OBJ and not passthrough: self.xmlMapper.pushObj(self) def objToXML(self, obj, output): """Outputs the <tagName>contents</tagName> into the stream.""" atts = self.objToAttsDict(obj) output(xmlo.StartElement(self.tagName, atts)) # Spit out the contained objects for element in self.getContained(obj): self.xmlMapper.put(element) # Get the hell out of Dodge output(xmlo.EndElement(self.tagName)) def XMLToObj(self, xmlObject): # Retreive the container tags, assume everything else will be # handled by other mappers, bitch if it isn't. if isinstance(xmlObject, xmlo.StartElement): if xmlObject.name != self.tagName: print "incoming: " + repr(xmlObject.name) print "handling: " + repr(self.tagName) raise CantHandleStreamError( "Can't handle '%s' begin tag in TagMapper for " "'%s'." % (xmlObject.name, self.tagName)) if self.started: raise CantHandleStreamError( "Can't nest '%s' elements in '%s' element w/ " "ContainerMapper; ContainerMapper is too " "stupid." % (self.tagName, self.tagName)) else: self.container = self.klass(*self.klassArgs, **self.klassDict) self.started = 1 # Do the attributes, if any self.xmlToObj(self.container, xmlObject) def presentObject(self, obj): """This default presentObject will .append(obj) to the container.""" self.container.append(obj) return 1 def endMapper(self): """Return the obj we constructed earlier (due to the simplicity of this mapper this method is easy).""" Mapper.endMapper(self) if not self.passthrough: self.xmlMapper.popObj() self.xmlMapper.propogateFinishedObject(self.container) def delimitedTextToObj(name, value): """Converts a name/value pair into a tuple, satisfying the constraints needed by SimpleDelimitedTextMapper's tagToObj converter function.""" return (name, value) def objToDelimitedText(obj): """Converts an 'obj' into a name/value pair, assuming it's a tuple created in the style of delimitedTextToObj. (In other words, it just returns obj.)""" return obj class SimpleDelimitedTextMapper(Mapper): """Maps the simplest case of <tag>content</tag>, with no attributes.""" def __init__(self, xmlMapper, textToObj = delimitedTextToObj, objToText = objToDelimitedText, tagName = None, klass = None, direction = None): """By default, returns (name, value) and takes in (name, value), though you can pass it functions to create an object and convert the object back to name/value.""" Mapper.__init__(self, xmlMapper, direction = direction) self.textToObj = textToObj self.objToText = objToText self.setTagName(tagName) def objToXML(self, obj, output): name, value = self.objToText(obj) output(xmlo.StartElement(name, {})) output(xmlo.CharacterData(value)) output(xmlo.EndElement(name)) def XMLToObj(self, xmlObject): if isinstance(xmlObject, xmlo.StartElement): self.setTagName(xmlObject.name) self.data = [] return if isinstance(xmlObject, xmlo.CharacterData): self.data.append(xmlObject.data) if isinstance(xmlObject, xmlo.StartCData): # Ignore this return if isinstance(xmlObject, xmlo.EndCData): # Ignore this return if isinstance(xmlObject, xmlo.EndElement): # Ignore this return # FIXME: Everything else should be an error in strict mode. def endMapper(self): """Create the object and return it.""" obj = self.textToObj(self.tagName.name, "".join(self.data)) self.xmlMapper.propogateFinishedObject(obj) def willMap(cls, xmlMapping, startElement): # If this is used as an ALL_TAG mapper, it will take anything return 1 willMap = classmethod(willMap) # This is a simple derived mapper, combining SimpleDelimitedTextMapper # with the ContainerMapper. class SimpleDictMapper(ContainerMapper): """A SimpleDictMapper maps the following into the obvious dict: <container> <name1>value</name1> <name2>value</name2> </container> goes to {'name1': 'value', 'name2': 'value'} It can be used for any key-like element by passing in appropriate getContained and overriding the endMapper and presentObject appropriately.""" def __init__(self, xmlMapper, klassArgs = (), klassDict = None, klass = dict, tagName = None, attributes = None, getContained = dict.iteritems, submappers = None, direction = None): if submappers is None: submappers = [] mapper = xmlmapping.MapDef(SimpleDelimitedTextMapper) mapper.update(tagName = ALL_TAGS, klass = tuple) submappers.append(mapper) ContainerMapper.__init__(self, xmlMapper, klassArgs, klassDict, klass, tagName, attributes, getContained, submappers, direction = direction) def endMapper(self): ContainerMapper.endMapper(self) def presentObject(self, obj): self.container[obj[0]] = obj[1] return 1 # For scope reasons, this needs to be out here def getgetattr(stringName): return lambda obj: getattr(obj, stringName) class MultipartMapper(Mapper): """A MultipartMapper maps some object to some multi-part XML representation that involves several sub-tags. For example, an HTML document consists of a <head> and a <body>. The MultipartMapper allows you to define several mappers that output in parts, so you can create an HTMLHead mapper and an HTMLBody mapper, and this will take care of sequencing the mappers. You'll probably need to subclass this and provide a real presentObject method for real uses, since there's no reasonable default logic for assembling the pieces for the XML->obj part. partMappers are provided as lists where the first element is a MapperDefinition for some part, and the second is a function that when passed the object for the mapper returns the part of the object to be processed. As a shortcut, you can pass in an object that when coerced into a string will identify the attribute of the object to handle, which is a common case. (str will be called on the object at construction time and stored, so you can't be dynamic with this.)""" # FIXME: Consider if there's a better way to assemble the parts def __init__(self, xmlMapper, klass = None, tagName = None, attributes = None, partMappers = None, passthrough = 0, direction = None): Mapper.__init__(self, xmlMapper, attributes = attributes, direction = direction) self.klass = klass self.setTagName(tagName) self.partMappers = partMappers[:] # Normalize the part mappers for partMapper in partMappers: try: # provide nicer error message if len(partMapper) != 2: # Append the identity function raise ValueError( "Can't create MultipartMapper because all " "provided partMappers must have two parts.") except TypeError, e: raise TypeError("Can't process partMapper %s" " because it is not a sequence;" " please see 'partMappers' docs." % partMapper) if callable(partMapper[1]): continue partMapper[1] = getgetattr(str(partMapper[1])) self.passthrough = passthrough if direction == XML2OBJ and not self.passthrough: self.xmlMapper.pushObj(self) self.started = 0 def objToXML(self, obj, output): """Outputs the object into the stream""" atts = self.objToAttsDict(obj) output(xmlo.StartElement(self.tagName, atts)) # For each part, spit out the contained objects for mapper, filter in self.partMappers: subObj = filter(obj) self.xmlMapper.pushMapper(mapper) self.xmlMapper.put(subObj) self.xmlMapper.popMapper(mapper) # End the xml output(xmlo.EndElement(self.tagName)) def XMLToObj(self, xmlObject): # When we get our start element, push all of our submappers # onto the XMLMapper at once. # Since as of right now we're not validating anything, we # don't care about the order. (Users are free to depend or # not as they see fit.) If we ever go to validation I think # we'll need to validate order, which makes this much uglier # (have to carefully push the submappers on only when # appropriate.) if isinstance(xmlObject, xmlo.StartElement): # Since we're pushing these on, we expect them to handle # the later XML, until the end tag self.xmlAttributes = xmlObject.attributes for mapper, filter in self.partMappers: self.xmlMapper.pushMapper(mapper) elif isinstance(xmlObject, xmlo.EndElement) or \ isinstance(xmlObject, xmlo.CharacterData): pass else: raise CantHandleStreamError( "ContainerMapper couldn't handle %s and it " "wasn't handled by anything else." % xmlObject) def endMapper(self): Mapper.endMapper(self) if not self.passthrough: self.xmlMapper.popObj() --- NEW FILE: xmlmappingconstants.py --- # Constants for the XMLMapper framework. ALL_ATTRIBUTES = 1 ALL_TAGS = "*" # Define whether the SimpleDelimitedTextMapper creates/works on a list # or a dict LIST = 0 DICT = 1 # These constants are used internally in XMLMapping to select # which implementation is used XML2OBJ = 0 OBJ2XML = 1 # These are names that the mappers might use that should NOT # be converted into a name.Name. They are symbolic constants # that represent special behaviors nameExceptions = [ ALL_TAGS ] # Exceptions class XmlMapperError(Exception): """An error has occurred while using the XML Mapper.""" class WrongClassError(XmlMapperError): """An instance of the wrong class was passed to something expecting a class.""" class CantHandleStreamError(XmlMapperError): """Something occurred in the stream that the XML Mapper doesn't know how to handle.""" class NoMappingKnownError(XmlMapperError): """The XMLMapper knows nothing about this class type.""" --- NEW FILE: README --- THIS LIBRARY IS CURRENTLY A DISASTER AREA. It may be interesting to examine the ideas but I wouldn't spend any time trying to use it. The idea was to create a way to specify one translation between some object and some XML representation of that object. Actually, I was technically successful. The problem is the resulting library is too impossibly complex for anyone to use... including me. At the moment, the code in here works, and I don't want to hold Iron Lute up on it. But I wouldn't poke around in here too much. --- NEW FILE: xmlmapping.py --- """WARNING: This module is currently a disaster. I believe there is value to be salvaged but I find it exceedingly unlikely that in its current state it could be useful to anyone. xmlmapping.py implements Yet Another XML marshalling scheme, but is different enough from all other Python XML libraries (I know of) to warrent creating it, rather then trying to tweak an existing library. xmlmapping concentrates on allowing the user to provide a description of how a given XML element maps into a Python object, and providing a reasonably automated way to convert back and forth. Advantages: * Both "in" and "out" are covered fully. * Mappings can themselves be used for translation; a single XML document can be mapped multiple ways, or a single object model can have multiple XML representations. * Exploit the symmetry of "in" and "out" without working too hard. * From an OO design point of view, an "XML Mapping" provides maximal decoupling between an object model and an XML document, so one's design need not hobble the other. (Indeed, Iron Lute's native file format, which is one of the formats driving the design of this library, bears almost no resemblence to the data structure used to store it in memory!) For "interesting" projects, I suspect this will be the strongest advantage. The module as currently written is a failure in these goals; it works but it is too hard to use. Since it's a failure, I'm not going too far into the structure of the current solution in case this is the one that gets released. The following is a note of my current thinking, intended to be decoded by me, not anybody else ;-), so don't sweat it too much. Instead of a given Mapper representing a Tag, it needs to represent a (tag, structuredtext) *tuple* where either can be elided. This simplifies a lot of things. In conjunction with a command based interface, where you request a mapper, receive it as a return value and manipulate it to the state you want, and have a lot of help, I think this gets to where we want. Eventually, this module is intended to be abstracted up into a specialized language, then from that language into a cross-language library that can be used with reasonable ease between languages, by implementing an interpreter for the language. We'll see how that goes, because I intend to play it by ear.""" # FIXME: We need a major terminology cleanup; "mapper" is being used # in two distinct concepts, XMLMapper and "mapper.py" # FIXME: Documentation fix: "By default, either a final object will be # output, OR all objects not caught by a top-level container will pop # out the other end of the stream." # MORE FIXME: Continuously specifying the namespace is a pain in the # ass, we need better context. # If this is true, we'll print the XML objects as they come in DEBUG = 0 import sys import os curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) curdir = os.path.normpath(curdir) if curdir not in sys.path: print curdir sys.path.append(curdir) del curdir import streams from streams import xmlobjects as xmlo, EndOfStream from xmlmappingconstants import * import names def strToObjStream(): """Convenience function: Returns a stream and a reference to the XMLMapping in the stream as a two-element tuple. You can then 'put' into the stream and will eventually get an object out of the stream.""" import streams s = streams.Stream() x = streams.ExpatXMLComponent() s.appendComponent(x) m = XMLMapping() s.appendComponent(m) return m, s def bases(obj): """Recursively yields all base classes of a given object.""" cls = obj.__class__ for c in recursivebases(cls): yield c def recursivebases(cls): """From a given class, keep recursing up the bases.""" yield cls for c in cls.__bases__: for c2 in recursivebases(c): yield c2 # FIXME: Ought to understand namespaces when specifying tags? class _XMLMapping_XML2Obj(streams.Component): """This private class implements an XML2Obj transform component. It takes XML objects (see xmlobject.py) in on the "put" method and outputs objects.""" def __init__(self, mapperDefinitions, namespaces = []): streams.Component.__init__(self) self.mapperDefinitions = mapperDefinitions[:] self.classToMapper = {} self.tagToMapper = {} self.tagStack = [] # Do the registration for mapdef in self.mapperDefinitions: self.pushMapper(mapdef) # mapperStack records the tag depth a mapper starts at and a # reference to the mapper itself. self.mapperStack = [] # The objStack tracks which objects have an # interest in receiving information about new # objects. It is a stack in the sense that a # earlier object can consume an event, but it # does not need to parallel the XML document # flow; it is managed indepedently. That's a # common case, so some support is provided, but # you are free to leave anything on this stack you # want. # Another common case will be to push a "collecter" # onto this stack that will collect all children # of some tag, allowing the end tag finalizer to # do something intelligent. self.objStack = [] self.started = 0 self.namespaces = namespaces self.initedStream = 0 def put(self, obj): """Manages feeding the Mappers, eventually outputting precisely one object to the end of the stream.""" # General procedure: Track the tag stack, allow Mappers to # override things and undo their changes when we roll the # stack back, collect enough info to know what mapper to call # next (observe start tags, may have to save them). if DEBUG: print obj if obj is EndOfStream: self.output(obj) return # If it's a namespace declaration, ignore it; we don't use # these if isinstance(obj, xmlo.StartNamespaceDecl) or \ isinstance(obj, xmlo.EndNamespaceDecl): return # if it's a start tag, push it onto the tag stack # and process it if isinstance(obj, xmlo.StartElement): name = obj.name = names.nameFromString(obj.name) # If we've got default tag accepters, ask them if they # want to handle this. mapperToCreate = None if ALL_TAGS in self.tagToMapper: # Ask the all_tag mappers if they want this start # element (klass is actually a tuple) for mapperDef in self.tagToMapper[ALL_TAGS]: if mapperDef.klass.willMap(self, obj): mapperToCreate = mapperDef break if mapperToCreate is None and name in self.tagToMapper: # We have a mapper for this tag # need to mark where we added this so we known when # to remove it later mapperToCreate = self.tagToMapper[name][-1] # FIXME: Need better way to handle mappers who want # multiple tags, like <children><child>2</child></children> # If all of these attempts fail... if mapperToCreate is None: raise NoMappingKnownError("Can't map <%s> to an " "object because no " "mapping is known." % name) newMapper = mapperToCreate.getMapper(self, XML2OBJ) self.mapperStack.append([newMapper, len(self.tagStack)]) self.tagStack.append(obj) # Now, if we have a mapper, feed this xmlobject to the mapper. try: self.mapperStack[-1][0].XMLToObj(obj) except IndexError: raise NoMappingKnownError("Can't map %s to an object " "because no mapping is known." % obj.name) # If this was an end element, pop it off if necessary if isinstance(obj, xmlo.EndElement): self.tagStack.pop() # If it's time to remove the last mapper, do so and # call the endMapper call if self.mapperStack[-1][1] == len(self.tagStack): self.mapperStack[-1][0].endMapper() self.mapperStack.pop() def pushObj(self, obj): """Pushes an object onto the propogateObject stack.""" self.objStack.append(obj) def popObj(self): """Pops an object from the propogateObject stack.""" self.objStack.pop() # FIXME: Rename to just "propogate" def propogateFinishedObject(self, obj): """Takes a finished object from a Mapper and propogates it up the stack until one of the other mappers says processing is complete. If the mapper stack has only one entry, assume this is the end of this mapper and go ahead and output the object on the stream. Returns true if it was handled, false otherwise.""" stackSize = len(self.objStack) if stackSize == 0: self.output(obj) return # Otherwise, call the presentObject method on each mapper # until someone claims it or it falls off the end for i in range(-1, -stackSize - 1, -1): result = self.objStack[i].presentObject(obj) if result: return result # Default handling goes here. No good ideas right now # Otherwise, nothing happened, so return 0. return 0 def pushMapper(self, mapperDef): """Takes a MapperDefinition and pushes it onto the active stack.""" if 'klass' in mapperDef.kwargs and \ mapperDef.kwargs['klass'] is not None: klass = mapperDef.kwargs['klass'] if klass in self.classToMapper: self.classToMapper[klass].append(mapperDef) else: self.classToMapper[klass] = [mapperDef] if 'tagName' in mapperDef.kwargs and \ mapperDef.kwargs['tagName'] is not None: tagName = mapperDef.kwargs['tagName'] if tagName in self.tagToMapper: self.tagToMapper[tagName].append(mapperDef) else: self.tagToMapper[tagName] = [mapperDef] def popMapper(self, mapperDef): """Removes mappers added with pushMapper.""" # FIXME: Why did I "del" the mappers? Because it should cause # an error if all the mappers are popped and something tries # to use one? In that case, that should be explicitly caught # and explicitly handled if 'klass' in mapperDef.kwargs and \ mapperDef.kwargs['klass'] is not None: klass = mapperDef.kwargs['klass'] mappers = self.classToMapper[klass] if len(mappers) == 1: del self.classToMapper[klass] else: mappers.pop() if 'tagName' in mapperDef.kwargs and \ mapperDef.kwargs['tagName'] is not None: tagName = mapperDef.kwargs['tagName'] mappers = self.tagToMapper[tagName] if len(mappers) == 1: del self.tagToMapper[tagName] else: mappers.pop() # FIXME: Ought to understand namespaces when specifying tags? class _XMLMapping_Obj2XML(streams.Component): """This (private) class implements the actual XMLMapping object.""" def __init__(self, mapperDefinitions, namespaces = []): streams.Component.__init__(self) self.mapperDefinitions = mapperDefinitions[:] self.classToMapper = {} self.tagToMapper = {} self.tagStack = [] # Do the registration for mapdef in self.mapperDefinitions: self.pushMapper(mapdef) self.objStack = [] self.started = 0 self.namespaces = namespaces self.initedStream = 0 def pushMapper(self, mapperDef): """Takes a MapperDefinition and pushes it onto the active stack.""" if 'klass' in mapperDef.kwargs and \ mapperDef.kwargs['klass'] is not None: klass = mapperDef.kwargs['klass'] if klass in self.classToMapper: self.classToMapper[klass].append(mapperDef) else: self.classToMapper[klass] = [mapperDef] if 'tagName' in mapperDef.kwargs and \ mapperDef.kwargs['tagName'] is not None: tagName = mapperDef.kwargs['tagName'] if tagName in self.tagToMapper: self.tagToMapper[tagName].append(mapperDef) else: self.tagToMapper[tagName] = [mapperDef] def popMapper(self, mapperDef): """Removes mappers added with pushMapper.""" # FIXME: Why did I "del" the mappers? Because it should cause # an error if all the mappers are popped and something tries # to use one? In that case, that should be explicitly caught # and explicitly handled if 'klass' in mapperDef.kwargs and \ mapperDef.kwargs['klass'] is not None: klass = mapperDef.kwargs['klass'] mappers = self.classToMapper[klass] if len(mappers) == 1: del self.classToMapper[klass] else: mappers.pop() if 'tagName' in mapperDef.kwargs and \ mapperDef.kwargs['tagName'] is not None: tagName = mapperDef.kwargs['tagName'] mappers = self.tagToMapper[tagName] if len(mappers) == 1: del self.tagToMapper[tagName] else: mappers.pop() def put(self, obj): # If we haven't output our namespaces yet, do so # (can't do this at __init__ because the components # that need to see these objects may not be in the # stream yet) # If this is the end of the stream, pass it through # FIXME: In strict mode, this should bitch if the XML is # invalid if obj is streams.EndOfStream: self.output(obj) return if not self.initedStream: for namespace, mnemonic in self.namespaces: self.output(xmlo.StartNamespaceDecl(mnemonic, namespace)) self.initedStream = 1 # Special case: If you want to output an XML tag directly, you # can. if isinstance(obj, xmlo.XMLBase): self.output(obj) return for klass in bases(obj): if klass in self.classToMapper: mapper = self.classToMapper[klass][-1].getMapper(self, OBJ2XML) mapper.objToXML(obj, self.output) return # Get the right class for the error message klass = type(obj) raise NoMappingKnownError("No XML Mapping is known for " "%s." % klass) class XMLMapping(streams.Component): """This class is configured to define a mapping, and returns an object that can actually do the mapping via calling getMapping(). Every instance of XMLMapping is a factory object for usable XMLMappings.""" # FIXME: namespaces allows you to specify extra namespaces in the # stream from the get-go def __init__(self, obj2XMLImplementation = _XMLMapping_Obj2XML, XML2ObjImplementation = _XMLMapping_XML2Obj, *extraArgs, **extraKeywords): self.obj2XMLImplementation = obj2XMLImplementation self.XML2ObjImplementation = XML2ObjImplementation self.extraArgs = extraArgs if extraKeywords is None: self.extraKeywords = {} else: self.extraKeywords = extraKeywords self.mapperDefinitions = [] def registerMapper(self, mapperDef): """Takes a mapper definition and registers it for use with this class.""" self.mapperDefinitions.append(mapperDef) def getXML2ObjMapping(self): """Returns an implementation of the mapper defined by the registerMapper calls, mapping XML to Objects.""" return self.XML2ObjImplementation(self.mapperDefinitions, *self.extraArgs, **self.extraKeywords) def getObj2XMLMapping(self): """Returns an implementation of the mapper defined by the registerMapper calls, mapping Objects to XML""" return self.obj2XMLImplementation(self.mapperDefinitions, *self.extraArgs, **self.extraKeywords) def objToStream(self, obj, stream): mapper = self.getObj2XMLMapping() return mapper.obj2XMLToStream(obj, stream) def objToText(self, obj): """Shortcut - render the object to XML directly""" mapper = self.getObj2XMLMapping() s = streams.Stream() s.appendComponent(mapper) s.appendComponent(streams.XMLToStringComponent()) s.put(obj) s.endStream() return ''.join(s) # The next two methods are OK to implement directly because they # implicitly do a call to 'getMapper' and do not store any state # in the current instance of XMLMapper. def textToObj(self, text): """Shortcut - convert the text into an object and return it.""" s = self.textStream() s.put(text) result = list(s) try: return result[0] except IndexError: return None def textStream(self): """Returns a stream suitable for putting XML text into and getting an object out of when you are done.""" s = streams.Stream() s.appendComponent(streams.ExpatXMLComponent()) s.appendComponent(self.getXML2ObjMapping()) return s class MapperDefinition(object): """A MapperDefinition provides an interface for constructing Mapper instances in pieces, instead of all at once, and provides an 'instantiate' method that returns a fully-constructed mapper object. args and kwargs are a list and a dict and may be directly manipulated. Both are meant for the actual Mapper instance that will be constructed. submappers may also be directly manipulated but it will be more syntactically convenient to use addSubmapper. A submapper is a mapper that will be pushed onto the XMLMapper stack when this mapper is instantiated, and popped off when it is done.""" def __init__(self, klass, kwargs = None, submappers = None): self.kwargs = kwargs if self.kwargs is None: self.kwargs = {} if submappers is None: submappers = [] self.submappers = submappers self.klass = klass def update(self, **kwargs): """Updates the kwargs as in the call.""" # Special case: If the kwargs has a tagName parameter, and # it's not an XML name, convert it to an XML name with no # namespace if 'tagName' in kwargs: if not isinstance(kwargs['tagName'], names.Name) and \ kwargs['tagName'] not in nameExceptions: tag = unicode(kwargs['tagName']) kwargs['tagName'] = names.nameFromString(tag) self.kwargs.update(kwargs) def addSubmapper(self, mapperDef): self.submappers.append(mapperDef) # FIXME: Is this ever used? I think this is a relic def addAtt(self, att): if not 'attributes' in self.kwargs: self.kwargs['attributes'] = [] self.kwargs['attributes'].append(att) def getMapper(self, xmlMapper, direction): """Returns a constructed Mapper object.""" kwargs = self.kwargs.copy() # submappers is treated specially since we don't want to pass # *anything* if the mapper doesn't want to take any submappers if self.submappers: kwargs['submappers'] = self.submappers kwargs['direction'] = direction try: return self.klass(xmlMapper, **kwargs) except TypeError: print self.klass raise # Alias for convenience; may be bad idea, but since many of these may # appear in a file... really need a minilanguage here. MapDef = MapperDefinition --- NEW FILE: __init__.py --- """This binds the XMLMapping library together.""" import sys import os curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) curdir = os.path.normpath(curdir) if curdir not in sys.path: print curdir sys.path.append(curdir) del curdir import names sys.modules['names'] = sys.modules['xmlmapping.names'] from xmlmapping import strToObjStream, XMLMapping, MapperDefinition, \ MapDef from mapper import Mapper, DummyMapper, ContainerMapper, delimitedTextToObj, \ objToDelimitedText, SimpleDelimitedTextMapper, \ SimpleDictMapper, MultipartMapper from attribute import Attribute, StringAtt, DictAtt from xmlmappingconstants import * # allow "from xmlmapping.xmlmappingconstants import *" import xmlmappingconstants --- NEW FILE: names.py --- """names.py creates a data structure for the handling of XML names in a namespaced world. It is primarily intended for the XMLMapping library but may concievably be useful in other contexts. FIXME: Is this a "leaf node"? The names module allows you to register namespaces and allows the creation of xml names based off of that namespace. The tags can then be compared with 'is', without you needing to manage the namespace issues yourself. Ideally, this will also cleanly handle 'psuedo-namespaces', XML documents that pre-date the namespace specification or are written as if they do, but that you may still want to 'pretend' are in a name space so you can use other namespaces later. No distinction is made between tags and attributes because at the moment, I can't find a reason to make one; dc:date is dc:date whether or not it's a tag or an attribute and the rest of the XML machinery distinguishes between the two for you. That makes using this much easier; if "dc" is a Namespace object, dc['date'] is pretty unambiguous. """ if __name__ == "names": import pdb pdb.set_trace() # A dict of namespaceUris -> Namespaces namespaces = {} class Namespace(object): """Represents an XML Namespace. Implemented as a singleton factory; the same namespace URL declaration will return the same namespace object. A namespaceUri of None is considered the same as an empty string.""" def __new__(cls, namespaceUri = ''): if namespaceUri is None: namespaceUri = '' try: ns = namespaces[namespaceUri] return ns except KeyError: pass self = object.__new__(cls) self.namespaceUri = namespaceUri namespaces[namespaceUri] = self self.names = {} return self def __getitem__(self, attr): """Get an XML name corresponding to the attr.""" try: return self.names[attr] except KeyError: pass name = Name(attr, self) self.names[attr] = name return name def __str__(self): return self.namespaceUri def __eq__(self, other): return self is other class Name(object): """A Tag represents an XML tag name, with the namespace. It can be compared to other tags. NOTE THIS IS NOT NORMALLY CONSTRUCTED DIRECTLY IF YOUR TAG IS NAMESPACED; it should be created via namespace['tagName']. Non-namespaced tags should be created with the namespace of 'None'""" def __init__(self, name, namespace = None): self.name = name self.namespace = namespace def __str__(self): if self.namespace is None or self.namespace.namespaceUri == '': return "%s" % self.name else: return "%s %s" % (self.namespace, self.name) def __repr__(self): return "Namespace(%r)[%r]" % (self.namespace.namespaceUri, self.name) def __eq__(self, other): return self is other or unicode(self) == unicode(other) def nameFromString(s): """Returns a Name object for the given string, assuming the string is a namespaceUri, followed by a space, followed by the tag name. This is the format given by expat and thus the xmlstreams.""" if not isinstance(s, basestring): import pdb pdb.set_trace() if " " not in s: # This is the null namespace return Namespace('')[s] else: ns, string = s.split() n = Namespace(ns) return n[string] |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:14
|
Update of /cvsroot/ironlute/ironlute/tests/miData/aDir In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/tests/miData/aDir Added Files: inDir.py inDir.pyc Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: inDir.pyc --- (This appears to be a binary file; contents omitted.) --- NEW FILE: inDir.py --- """A file in a subdirectory that isn't a module.""" value = 1 |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:14
|
Update of /cvsroot/ironlute/ironlute/tests/miData In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/tests/miData Added Files: aModule.py allThree.py allThree.pyo miTarget1.py pyconly.pyc pyoonly.pyo Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: miTarget1.py --- """This is an empty module to use to test moduleIndexer.""" target = 1 --- NEW FILE: allThree.py --- """Make a pyc, py, and pyo file for testing the import logic.""" value = 1 --- NEW FILE: pyoonly.pyo --- (This appears to be a binary file; contents omitted.) --- NEW FILE: allThree.pyo --- (This appears to be a binary file; contents omitted.) --- NEW FILE: aModule.py --- """test target for isModule""" value = 1 class TestClass(str): IndexerMetadata = {"name": "TestClass"} IndexerKey = "test" --- NEW FILE: pyconly.pyc --- (This appears to be a binary file; contents omitted.) |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:14
|
Update of /cvsroot/ironlute/ironlute/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/tests Added Files: moduleIndexerTest.py outlineTest.py registryTest.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: registryTest.py --- """Test the registry.""" import unittest import sys import os if __name__ != "__main__": f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: if os.pardir not in sys.path: sys.path.append(os.pardir) import types import registry import cPickle import userDirectory import warnings Table = registry.Table class RegitryTest(unittest.TestCase): """Does the registry work as expected?""" def testRegistryUse(self): """Registry: Works as expected in memory?""" root = Table() # test __setattr__ && __getattr__ self.assertRaises(KeyError, root.__getattr__, "a") root.a = 1 self.assert_(root.a == 1) # __setattr__ won't override class elements for name in dir(Table): self.assertRaises(ValueError, root.__setattr__, name, 1) # test __getitem__, __setitem__, unicodification self.assert_(root["a"] == 1) self.assertRaises(KeyError, getattr, root, "b") root["b"] = 2 self.assert_(root["b"] == 2) self.assert_(root.b == 2) # We can set any name here we want via [] checkNames = dir(Table) checkNames.extend(['has.period', u'\u1234', u'\u1235.\u1236']) for name in checkNames: root[name] = 1 self.assert_(root[name] == 1) # Don't create tables if "." in name: first = name[:name.find(".")] try: self.assertRaises(KeyError, getattr, root, first) except UnicodeEncodeError: #print "Wha? %r" % first # I don't know why this happens but a manual test # passes pass # Unicode everything root[23] = 1 self.assert_(root["23"] == 1) self.assert_(root[u"23"] == 1) root[u"\u1200"] = 1 self.assert_(root[u"\u1200"] == 1) # Test initSubkey, with and without a dict arg, and w/ # multiple depths root.initSubkey("arg", 11) self.assert_(root.arg == 11) root.initSubkey("arg", 12) self.assert_(root.arg == 11) root.initSubkey("l1a.l2a", 13) self.assert_(root.l1a.l2a == 13) root.initSubkey("l1a.l2a", 14) self.assert_(root.l1a.l2a == 13) root.initSubkey("l1b", {'a':2, 'b':4}) self.assert_(root.l1b.a == 2) self.assert_(root.l1b.b == 4) root.initSubkey("l1b", {'a':3, 'b':5}) self.assert_(root.l1b.a == 2) self.assert_(root.l1b.b == 4) root.initSubkey("l1c.l2c", {'a':2, 'b':4}) self.assert_(root.l1c.l2c.a == 2) self.assert_(root.l1c.l2c.b == 4) root.initSubkey("l1c.l2c", {'a':3, 'b':5}) self.assert_(root.l1c.l2c.a == 2) self.assert_(root.l1c.l2c.b == 4) # Test setdefault sk = root.initSubkey("setdefaulttest", {}) self.assertRaises(KeyError, sk.__getitem__, "a") self.assert_(sk.setDefault("a", 1) == 1) self.assert_(sk.a == 1) self.assert_(sk.setdefault("a", 2) == 1) def testRegistryPickles(self): """Registry: Pickles correctly?""" root = Table() root.a = 1 root.b = 2 root.c = {} root.c.a = 1 rootStr = cPickle.dumps(root) root2 = cPickle.loads(rootStr) self.assert_(root2.a == 1) self.assert_(root2.b == 2) self.assert_(root2.c.a == 1) # DUE TO LACK OF SAFETY there is no safe, cross-platform way # to test the saving and the loading of the registry. If you trust # this system create a file in your Iron Lute user directory, # sub-directory "testdata" and name it "trusted". If it exists, # this will go ahead and use os.tmpfilename to create temporary # files. If you are not changing registry.py, or you do not # understand the security implications of this, do not do this. # (Note, this is mostly a UNIX issue, but I don't know for certain # that Windows is immune and I have no idea about OSX.) def testLoadingAndSaving(self): # FIXME: Factor this safety check out filename = os.path.join(userDirectory.getAppDir('Iron Lute'), "testdata", "trusted") trusted = 1 try: f = file(filename) f.close() except IOError: trusted = 0 if not trusted: warnings.warn("Loading and saving the registry not " "tested due to security concerns; see " "comments in registryTest.py for more " "information. To trust this test and " "run it, create a file at '%s'." % filename) return filename = os.tmpnam() try: root = registry.registry(filename) root.a = 1 root.saveToFile() del root root = registry.registry(filename) self.assert_(root.a == 1) del root finally: try: os.unlink(filename) except OSError: pass if __name__ == "__main__": unittest.main() --- NEW FILE: moduleIndexerTest.py --- """Tests the module indexer.""" import unittest import sys import os if __name__ != "__main__": f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: if os.pardir not in sys.path: sys.path.append(os.pardir) # Special to this testing: Ensure the test data directory is at # the start of the path dir, filename = os.path.split(__file__) testDataDir = os.path.join(dir, "miData") from moduleIndexer import * import moduleIndexer import registry class ModuleIndexerTest(unittest.TestCase): def setUp(self): # Make sure the test directory is first in the path self.realModules = sys.modules.copy() if sys.path[0] <> testDataDir: sys.path.insert(0, testDataDir) # Save the dirsForAllIndices self.dirsForAllIndices = moduleIndexer.dirsForAllIndices moduleIndexer.dirsForAllIndices = [] def tearDown(self): # Clear the test data dir for paranoia's sake if sys.path[0] == testDataDir: sys.path.pop(0) sys.modules = self.realModules moduleIndexer.dirsForAllIndices = [] def testGetModule(self): """moduleIndexer: module loading works correctly""" index = Index(registry.Table(), None) testModule = os.path.join(testDataDir, "miTarget1.py") name, module, imported = index._getModule(testModule) self.assert_(imported) self.assert_(module.target == 1) name, module, imported = index._getModule(testModule) self.assert_(not imported) self.assert_(module.target == 1) testModule = os.path.join(testDataDir, "pyconly.pyc") name, module, imported = index._getModule(testModule) self.assert_(imported) self.assert_(module.value == 1) def testIsModule(self): """moduleIndexer: isModule works correctly""" # We test with real files, even though isModule doesn't hit # the file system for two reasons: Check that the os.path.join # doesn't confuse it, and for if it ever does. test = lambda f, r: \ self.assert_(isModule(os.path.join(testDataDir, f)) == r) test("allThree.py", True) test("allThree.pyc", True) #test("allThree.pyo", True) test("notAModule.txt", False) # Check the __init__.py exclusions test("__init__.py", False) test("__init__.pyc", False) def testScanner(self): """moduleIndexer: directory scanner works correctly""" index = Index() changed, unchanged = index._scanDirectory(testDataDir) # Running this via runAllTests has interactions with # importing the moduleIndexer for the outline # module # FIXME: Write a test runner that can run a seperate # instance of Python for a test, because moduleIndexer # really needs that if __name__ == "__main__": self.assert_(len(changed) == 5) self.assert_(len(unchanged) == 0) else: self.assert_(len(changed) == 0) self.assert_(len(unchanged) == 0) modules = [x[7:] for x in changed] modules.sort() self.assert_(modules == ['aDir/inDir', 'aModule', 'allThree', 'miTarget1', 'pyconly']) # Check that the mtime is recorded correctly self.assert_(getLatestMTime(os.path.join(testDataDir, 'aModule')) == index.reg.dirs['miData']['aModule']) # If we fudge one of the dates into the far future, validate # that the module isn't loaded r = registry.Table() index = Index(r) r.initSubkey("dirs.miData.aModule", 999999999999L) changed, unchanged = index._scanDirectory(testDataDir) self.assert_(len(changed) == 4) self.assert_(len(unchanged) == 1) def testModuleScanning(self): """moduleIndexer: validate scanning works""" index = Index() index.predicate = lambda s: 1 index.addDirectory(testDataDir) # test that IndexerKey works testObject = index.test.TestClass() self.assert_(isinstance(testObject, str)) if __name__ == "__main__": unittest.main() --- NEW FILE: outlineTest.py --- """Tests the outline module.""" import unittest import sys import os if __name__ != "__main__": f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: if os.pardir not in sys.path: sys.path.append(os.pardir) import outline from outline import quickMakeNodes from constants import * import copy import pdb [...976 lines suppressed...] gotEvent = GotEvent() handle.subscribe(gotEvent) handle.node.setData("a") self.assert_(gotEvent.gotEvent == 1) self.assert_(gotEvent.event.sender is handle.node) self.assert_(gotEvent.event.sourceLink is handle.link) self.assert_(gotEvent.event.sourceHandle is handle) # Make sure that if a link is detached it is unsubscribed # correctly. oldNode = handle.node link = handle.link link.dest = None gotEvent = GotEvent() oldNode.setData("b") self.assert_(gotEvent.gotEvent == 0) if __name__ == "__main__": unittest.main() |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:13
|
Update of /cvsroot/ironlute/ironlute/streams/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/streams/tests Added Files: __init__.py componentTest.py componentTestBase.py outlinestreamTest.py socketstreamTest.py streamTest.py xmlcomponentTest.py xmlobjectTest.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: __init__.py --- # External users may wish to use this to test from componentTestBase import ComponentTestBase --- NEW FILE: xmlcomponentTest.py --- """Tests the XMLComponent to make sure it works as expected.""" import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.join(os.getcwd(), os.path.dirname(f))) if f not in sys.path: sys.path.append(f) f = f[:f.rfind(os.sep)] if f not in sys.path: sys.path.append(f) f = f[:f.rfind(os.sep)] if f not in sys.path: sys.path.append(f) else: sys.path.append("..") import stream import xmlcomponent from xmlobjects import * import streamconstants from xml.parsers.expat import ExpatError import componentTestBase xmldocument = """<?xml version="1.0"?> <!DOCTYPE A SYSTEM "bleh.dtd"> <?argle flingle?> <root xmlns="ns1" xmlns:foo="ns2"> W <!-- argle --> <![CDATA[ < < < ]]> </root> """ xmldocumentbad = """ <?xml version="1.0"?> <root></root>""" namespaced = """<?xml version="1.0"?> <root xmlns='http://www.slashes.com/::/' xmlns:foo='ns2'> <munch1 foo:tiddle='man'>hello</munch1> <foo:bungle testatt1='moo'/> </root> """ # This may be fragile, if Expat changes desiredResult = [ XMLDeclaration('1.0', None, -1), StartDocType('A', 'bleh.dtd', None, 0), EndDocType(), ProcessingInstruction("argle", "flingle"), StartNamespaceDecl(None, 'ns1'), StartNamespaceDecl('foo', 'ns2'), StartElement('ns1 root', {}), CharacterData('\n'), CharacterData('W'), CharacterData(' '), Comment(' argle '), CharacterData('\n'), StartCData(), CharacterData("\n"), CharacterData("< < <"), CharacterData("\n"), EndCData(), CharacterData("\n"), EndElement('ns1 root'), EndNamespaceDecl('foo'), EndNamespaceDecl(None) ] class TestExpat(componentTestBase.ComponentTestBase): __component__ = xmlcomponent.ExpatXMLComponent class TestXmlToString(componentTestBase.ComponentTestBase): __component__ = xmlcomponent.XMLToStringComponent class TestXMLComponent(unittest.TestCase): """Tests the XML component.""" def testXMLComponent(self): """Streams: XML Component works""" s = stream.Stream() s.appendComponent(xmlcomponent.ExpatXMLComponent()) s.put(xmldocument) result = list(s) if hasattr(result[-1], 'originalError'): raise result[-1].originalError self.assert_(len(result) == len(desiredResult), "%s instead of %s obtained (wrong len)" % (result, desiredResult)) for i in range(len(result)): self.assert_(result[i].sameAs(desiredResult[i]), "%s and %s differ" % (result[i], desiredResult[i])) def testInitAndContextSync(self): """Streams: XML Component defaults in sync with XMLContext""" # We want the default arguments on XMLToStringComponent # to synchronize with the default arguments in the # XMLContext it creates; this validates that component = xmlcomponent.XMLToStringComponent() context = XMLContext() self.assert_(context.autoCreateMnemonics == component.context.autoCreateMnemonics) self.assert_(context.verifyTags == component.context.verifyTags) # FIXME: Some good way to see the other args that may be added # automatically? def testXMLComponentBad(self): """Streams: XML Component fails correctly""" s = stream.Stream() s.appendComponent(xmlcomponent.ExpatXMLComponent()) self.assertRaises(ExpatError, s.put, xmldocumentbad) result = list(s) self.assert_(s.status == streamconstants.STREAM_ERROR) self.assert_(len(result) == 1) self.assert_(hasattr(result[0], 'originalError')) def testXMLComponentNamespaces(self): """Streams: XML Component handles namespaces properly?""" s = stream.Stream() s.appendComponent(xmlcomponent.ExpatXMLComponent()) s.put(namespaced) tokens1 = list(s) s2 = stream.Stream() s2.appendComponent(xmlcomponent.XMLToStringComponent()) s2.multiPut(tokens1) result = list(s2) xml2 = ''.join(result) # This should end up with the exact same XML stream as # the original parse s3 = stream.Stream() s3.appendComponent(xmlcomponent.ExpatXMLComponent()) s3.put(xml2) tokens2 = list(s3) self.assert_(len(tokens1) == len(tokens2)) # Check that the new xml is regenerated if we run it through # again s4 = stream.Stream() s4.appendComponent(xmlcomponent.XMLToStringComponent()) s4.multiPut(tokens2) xml3 = ''.join(list(s4)) self.assert_(xml3 == xml2) if __name__ == "__main__": unittest.main() --- NEW FILE: componentTestBase.py --- import unittest import os import sys if __name__ != "__main__": # add parent and grandparent to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) import stream class ComponentTestBase(unittest.TestCase): """Tests that all components must pass. Set the component your class is testing in the __component__ class attribute, args in __args__, and kwargs in __kwargs__ (if applicable). If the string "stream" is given as a argument value, it will be replaced by the stream created for the testing.""" __component__ = None __args__ = () __kwargs__ = {} def __init__(self, methodName = 'runTest'): unittest.TestCase.__init__(self, methodName) self._testMethodDoc = getattr(self, methodName).__doc__ def testEndOfStream(self): """Streams: [Component] handles EndOfStream without excepting""" s = stream.Stream() args = list(self.__args__[:]) kwargs = self.__kwargs__.copy() for i in range(len(args)): if args[i] == 'stream': args[i] = s for key in kwargs.keys(): if kwargs[key] == 'stream': kwargs[key] = s s.appendComponent(self.__component__(*args, **kwargs)) # Should not raise exception s.endStream() def shortDescription(self): """Overrides the description given when testing with the -v option to add the relevant component name in instead of [Component].""" doc = self._testMethodDoc doc = doc.replace("[Component]", self.__component__.__name__) return doc and doc.split("\n")[0].strip() or None --- NEW FILE: componentTest.py --- """Tests the utility streams.""" import unittest import sys import os if __name__ != "__main__": f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) sys.path.append("..") else: sys.path.append("..") import stream import component import utilcomponents import componentTestBase import zlib import pdb testData = [1, 2, 3] #Tests for the LineFeedNormalizer: a sequence of sequence # of two sequences, where the first sequence is the test # data and the second is the output. LineFeedTests = [ [['a'], ['a']], [['a\n'], ['a\n']], [['a\r'], ['a', '\n']], [['a\r\n'], ['a\n']], [['a\r', '\nb'], ['a', '\nb']], [[u'a'], [u'a']], [[u'a\n'], [u'a\n']], [[u'a\r'], [u'a', u'\n']], [[u'a\r\n'], [u'a\n']], [[u'a\r', u'\nb'], [u'a', u'\nb']] ] class LineFeedNormalizerTests(componentTestBase.ComponentTestBase): """Tests that the LineFeedNormalizer works as advertised.""" __component__ = utilcomponents.LineFeedNormalizer def testLineFeedNormalizer(self): """Streams: [Component] works""" i = 0 for test in LineFeedTests: input, desiredOutput = test s = stream.Stream() s.appendComponent(utilcomponents.LineFeedNormalizer()) for element in input: s.put(element) s.endStream() output = list(s) self.assertEquals(output, desiredOutput) self.assertEquals([type(x) for x in output], [type(x) for x in desiredOutput]) # "Type test %s failed" % i) i = i + 1 class ThreadTester(unittest.TestCase): # Can't use ComponentTestBase because it can't handle the # threader, which changes the interface on the stream (adding # "join", etc) so we have to test on our own def testThreader(self): """Streams: Threader works""" s = stream.Stream() s.appendComponent(utilcomponents.Threader(s)) s.multiPut(testData) s.join() result = list(s) self.assert_(result == testData) def testThreaderHandlesEOS(self): """Streams: Threader handles EndOfStream without excepting""" s = stream.Stream() s.appendComponent(utilcomponents.Threader(s)) s.endStream() s.join() result = list(s) self.assert_(result == []) class ZLibCompressTest(componentTestBase.ComponentTestBase): __component__ = utilcomponents.ZLibCompressor class ZLibDecompressTest(componentTestBase.ComponentTestBase): __component__ = utilcomponents.ZLibDecompressor # Should be a unit test, doesn't work and not currently importent. class ZLibValidator(object): def makeStream(self, codec = None): s = stream.Stream() s.appendComponent(utilcomponents.ZLibCompressor(codec)) s.appendComponent(utilcomponents.ZLibDecompressor(codec)) return s def makeTransparentStream(self): s = stream.Stream() s.appendComponent(utilcomponents.ZLibDecompressor(transparent = 1)) return s def loadData(self): if hasattr(self, "data"): return self.data f = file(__file__) self.data = f.read() f.close() return self.data def testZLibInGeneral(self): """Streams: ZLib system basically works""" s = self.makeStream() d = self.loadData() s.put(d) s.endStream() self.assert_(d == "".join(s)) # Test the Unicode characteristics. unicodeTest = u'This is a string w/ a Unicode ugly in it. \u1234' s = self.makeStream() self.assertRaises(UnicodeEncodeError, s.put, unicodeTest) s = self.makeStream('utf-8') s.put(unicodeTest) s.endStream() self.assert_(unicodeTest == u"".join(s)) def testZLibDecompressTransparent(self): """Streams: Transparent ZLib Decompression works""" s = self.makeTransparentStream() # Test the general case d = self.loadData() s.put(d) s.endStream() mydata = ''.join(s) self.assert_(d == mydata) # Test a small case; not enough data to do the test, # comes out other end s = self.makeTransparentStream() s.put('a') s.endStream() contents = ''.join(s) self.assert_('a' == contents) # Test w/ real compression data, putting it in two bytes # at a time and validating stream correctly decompresses it s = self.makeTransparentStream() data = "123" compressed = zlib.compress(data) for idx in range(0, len(compressed), 2): s.put(compressed[idx:idx+2]) s.endStream() contents = ''.join(s) print "|", repr(contents), "|" self.assert_(data == contents) if __name__ == "__main__": unittest.main() --- NEW FILE: xmlobjectTest.py --- """Tests the xmlobjects, mostly just the XMLContext class. (The XML objects themselves are mostly auto-generated and extremely simple; there's not much to test, at least that I've thought of.""" import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) sys.path.append("..") else: sys.path.append("..") from xmlobjects import * class TestXMLContext(unittest.TestCase): def testXMLContextObject(self): """Streams: XMLContext namespace handling works correctly""" context = XMLContext() # Start simply: Present a simple start tag, make sure # no namespaces appear aStart = StartElement('a', {}) context.present(aStart) # No namespaces were stored... self.assert_(len(context.namespaces) == 0) # There's a tag on the stack self.assert_(len(context.tagStack) == 1) # and it's the one we just put on... self.assert_(context.tagStack[0] is aStart) # OK, that's about as simple as it comes. Test explicit # namespaces with mnemonics: xmlnsb = "xmlns:b" bUrl = "http://www.b.com/b" bStart = StartElement('b', {xmlnsb: bUrl}) context.present(bStart) # Now present a tag "bUrl c" and be sure it is converted cStart = StartElement("%s c" % bUrl, {}) context.present(cStart) # Assert the name was modified to fit the new XML namespace self.assert_(cStart.name == "b:c") # Terminate the c and b tags, then ensure that the namespace # is forgotten cEnd = EndElement("c") context.present(cEnd) bEnd = EndElement("b") context.present(bEnd) self.assert_(not context.namespaces[bUrl]) # Present a StartNamespaceDecl object and make sure it tacks # the correct atts onto the next start tag dUrl = "http://www.d.com/d" nsD = StartNamespaceDecl("d", dUrl) context.present(nsD) # Send in a tag to be modified eStart = StartElement('e', {}) context.present(eStart) # Validate the correct attribute has been added self.assert_(eStart.attributes['xmlns:d'] == dUrl) # Send in another tag and validate it does NOT # get that attribute added fStart = StartElement('f', {}) context.present(fStart) self.assert_('xmlns:d' not in fStart.attributes) # If a default xmlns is declared, make sure subsequent tags # with that namespace are correctly not colon'ed: gUrl = "http://www.g.com/g" gStart = StartElement('g', {'xmlns': gUrl}) context.present(gStart) hStart = StartElement('%s h' % gUrl, {}) context.present(hStart) self.assert_(hStart.name == 'h') # Nesting: Ensure that nested namespace declarations override # previous declarations, and are restored appropriately. jUrl = "http://www.j.com/j" iStart = StartElement("i", {'xmlns': jUrl}) context.present(iStart) jStart = StartElement("%s j" % jUrl, {}) context.present(jStart) self.assert_(jStart.name == 'j') # Now pop j and i off, and make sure the g default namespace # resumes taking hold jEnd = EndElement('i') context.present(jEnd) iEnd = EndElement('i') context.present(iEnd) kStart = StartElement('%s k' % gUrl, {}) context.present(kStart) self.assert_(kStart.name == 'k') # Verify that mnemonics declared in the tag take IMMEDIATE # effect lUrl = "http://www.l.com/l" lStart = StartElement("%s l" % lUrl, {'xmlns:l': lUrl}) context.present(lStart) self.assert_(lStart.name == 'l:l') # Check default one specially since it is handled specially mUrl = 'http://www.m.com/m' mStart = StartElement("%s m" % mUrl, {'xmlns': mUrl}) context.present(mStart) self.assert_(mStart.name == 'm') # For two identical namespaces, one is chosen arbitrarily nUrl = "http://www.n.com/n" nStart = StartElement("%s n" % nUrl, {'xmlns:n1': nUrl, 'xmlns:n2': nUrl}) context.present(nStart) self.assert_(nStart.name[0] == 'n') def testXMLContextValidation(self): """Streams: XML Context validation works""" # Validate that non-verification throws no exceptions context = XMLContext(verifyTags = 0) context.present(StartElement('a', {})) context.present(EndElement('b', {})) # Validate that verification works context = XMLContext(verifyTags = 1) context.present(StartElement('a', {})) self.assertRaises(WrongClosingTagError, context.present, EndElement('b')) def testXMLContextAutoNamespaces(self): """Streams: XML Context auto-namespacing works""" # Validate the strict case: non-declared namespaces # in atts or names raise NoNamespaceError exceptions context = XMLContext(autoCreateMnemonics = 0) self.assertRaises(NoNamespaceError, context.present, StartElement('http://www/ n', {})) self.assertRaises(NoNamespaceError, context.present, StartElement('w', {'http://www/ n': '2'})) # First, does the auto-creation work at all? context = XMLContext(autoCreateMnemonics = 1) # this is the same mnemonic xmlobjects.py will choose targetMnemonic = getNewMnemonic(context.mnemonics) aUrl = "http://www.a.com/a" aStart = StartElement("%s a" % aUrl, {}) context.present(aStart) self.assert_(aStart.name == '%s:a' % targetMnemonic) # Alrighty, we get some tricky cases in here, with mnemonics # obscuring previous namespaces and thus needing the old # namespaces to be re-mnemonicked (to coin a word) # Declare a namespace http://www.b.com/b with mnemonic 'b' bUrl = "http://www.b.com/b" bStart = StartElement('b', {'xmlns:b': bUrl}) context.present(bStart) # Obscure it with http://www.c.com/c, but with same mnemonic # 'b' cUrl = "http://www.c.com/c" cStart = StartElement('c', {'xmlns:b': cUrl}) context.present(cStart) # Now, validate that a new namespace mnemonic is created if we # try to add bUrl again, because the mnemonic is obscured dStart = StartElement('%s d' % bUrl, {}) targetMnemonic = getNewMnemonic(context.mnemonics) context.present(dStart) self.assert_(dStart.name == "%s:d" % targetMnemonic) # And validate that once this is all popped off, the 'b' # matching bUrl still works correctly. dEnd = EndElement('%s d' % bUrl) context.present(dEnd) cEnd = EndElement('c') context.present(cEnd) eStart = StartElement('%s e' % bUrl, {}) context.present(eStart) self.assert_(eStart.name == 'b:e') # I think that largely captures the ways this could # fail... (later) but I was of course wrong. # If the root element is in the base namespace, check that we # don't create a mnemonic for it. context = XMLContext(autoCreateMnemonics = 1) aStart = StartElement("bleh", {}) self.assert_(len(context.mnemonics) == 0) context.present(aStart) self.assert_(len(context.mnemonics) == 0) if __name__ == "__main__": unittest.main() --- NEW FILE: outlinestreamTest.py --- """Tests the outline stream: Does it correctly reconstruct the outline and maintain the other properties we are looking for?""" import unittest import sys import os if ".." not in sys.path: sys.path.append("..") if "../.." not in sys.path: sys.path.append("../..") import outlinestream import outline import stream import gc # In the implementation file, the extended names are worth # it for clarity. Here, they get in the way since we use them # directly a lot elFromNode = outlinestream.createElementFromNode elFromLink = outlinestream.createElementFromLink # We need permutations to test the element to node generator: we # render documents to their elements, then check that all possible # permutations of those elements correctly re-assemble the source # document; pretty darned hard testing, IMHO # General permutation code pulled from # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/190465 # because I was too lazy to reconstruct it myself :-) def permutationsRecurse(items, n): if not n: yield [] else: for i in xrange(len(items)): for j in permutationsRecurse(items[:i]+items[i+1:],n-1): yield [items[i]]+j def permutations(items): return permutationsRecurse(items, len(items)) class TestOutlineStream(unittest.TestCase): """Does the outline streamer work?""" def testElementGeneration(self): """Outline Streams: Does element generation work?""" data = "Hello" node1 = outline.BasicNode(data) node2 = node1.addNewChild() # element node 1 args = (node1.id, outline.BasicNode.classId, data, [node1.data.outgoing[0].id]) en1 = outlinestream.NodeElement(*args) element1 = elFromNode(node1) # If __eq__ fails this helps track down where self.assert_(en1.id == element1.id) self.assert_(en1.nodeType == element1.nodeType) self.assert_(en1.data == element1.data) self.assert_(en1.children == element1.children) self.assert_(en1 == elFromNode(node1)) # Validate all fields are being checked by __eq__ for i in range(len(args)): newargs = list(args) newargs[i] = None self.assert_(en1 != elFromNode(node1), "Field %s not " "checked by __eq__ on NodeElement" % i ) # Now check links link = node1.data.outgoing[0] el1 = outlinestream.LinkElement(link.id, link.source.id, link.dest.id) self.assert_(el1 == elFromLink(link)) def testBasicNode(self): """Outline Streams: Does the basic node work correctly?""" node1 = outline.BasicNode() nodeElement = elFromNode(node1) node2 = nodeElement.node() self.assert_(node1.equivalentTo(node2)) # Test with data data = u"Test Data" node1 = outline.BasicNode(data=data) nodeElement = elFromNode(node1) node2 = nodeElement.node() self.assert_(node1.equivalentTo(node2)) def testSimpleRelationship(self): """Outline Streams: Does A->B convert to elements correctly?""" node1 = outline.BasicNode() s = stream.Stream() s.appendComponent(outlinestream.NodeToElements()) s.put(node1) result = list(s) self.assert_(result == [elFromNode(node1)]) # Test with A->B node2 = node1.addNewChild(childKwArgs={'data': '2'}) s = stream.Stream() s.appendComponent(outlinestream.NodeToElements()) s.put(node1) result = list(s) self.assert_(result == [elFromNode(node1), elFromLink(node1.data.outgoing[0]), elFromNode(node2)]) # Test that a recursive structure doesn't infinitely loop outline.Link(node2, node1) s = stream.Stream() s.appendComponent(outlinestream.NodeToElements()) s.put(node1) result = list(s) self.assert_(result == [elFromNode(node1), elFromLink(node1.data.outgoing[0]), elFromNode(node2), elFromLink(node2.data.outgoing[0])]) def newElementStream(self): s = stream.Stream() component = outlinestream.ElementsToNode() s.appendComponent(component) return s, component def testReconstructorSimpler(self): """Outline Streams: Basic reconstructor works?""" # Let's start with a single node, no children node1 = outline.BasicNode() element1 = elFromNode(node1) s, c = self.newElementStream() s.put(element1) s.endStream() result = list(s) self.assert_(result[0].equivalentTo(node1)) # Alright, let's go with a simple A->B node2 = node1.addNewChild() # Note: Element1 is now out of date, because the node has # children now # FIXME: Should elements stay up to date? (Hard problem) element1 = elFromNode(node1, 1) element2 = elFromNode(node2) element3 = elFromLink(node1.data.outgoing[0]) # Validate that all permutations of this work elements = [element1, element2, element3] for order in permutations(elements): #for order in [[element3, element1, element2]]: s, component = self.newElementStream() s.multiPut(order, endStream = 1) result = list(s)[0] self.assert_(result.equivalentTo(node1, 1), "Reassembly failed for order %s" % order) self.assertClean(component) def testTorturously(self): """Outline Stream: Torture test disassembly->reconstruction""" def testNode(n): """For the given node n, test that the outlinestream correct disassembles and reassembles it such that the output is .equivalentTo the input.""" s = stream.Stream() s.appendComponent(outlinestream.NodeToElements()) s.put(n) elements = list(s) s = stream.Stream() c = outlinestream.ElementsToNode() s.appendComponent(c) for order in permutations(elements): s, component = self.newElementStream() s.multiPut(order, endStream = 1) result = list(s)[0] self.assert_(result.equivalentTo(n), "Order failed: %s" % order) self.assertClean(component) # Simple test node = outline.BasicNode(data = "Hello!") testNode(node) # Self-linking test node.addChild(node) testNode(node) # A->B->C->A node = outline.quickMakeNodes([['b', 'c']]) node.setData('a') node[0][0].addChild(node) # C->A testNode(node) # A, B, C, and D in diamond (A->B->D and A->C->D) # skipping this test because it takes too long to # test 8! different combinations; try again if we ever # C-ify or Pyrex this. #node = outline.BasicNode('a') #node.addNewChild(childArgs=('b',)) #node.addNewChild(childArgs=('c',)) #node[0].addNewChild(childArgs=('d',)) #node[1].addChild(node[0][0]) #testNode(node) def assertClean(self, component): self.assert_(component.unresolvedLinks == {}) self.assert_(component.pendingLinks == {}) self.assert_(component.pendingNodes == {}) if __name__ == "__main__": unittest.main() --- NEW FILE: streamTest.py --- """Tests the streams to make sure they work in all basic combinations. If run directly, it runs all the tests.""" import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) sys.path.append("..") else: sys.path.append("..") import stream import component # Since the streams are all defined in terms of Python objects, we can # get away with testing on a very simple input data. testData = [1, 2, 3] class SimpleComponent(component.Component): """A simple component used to check the order of the components while being added.""" def put(self, obj): self.stream.output(obj + '1') class SimpleStreamTest(unittest.TestCase): """Tests simple streams""" def testSimpleStream(self): """Streams: basic streams work""" s = stream.Stream() s.multiPut(testData) result = list(s) self.assert_(result == testData) components = s.getComponentList() self.assert_(len(components) == 0) # Test appending components newcomponent1 = SimpleComponent() s.appendComponent(newcomponent1) components = s.getComponentList() self.assert_(components == [newcomponent1]) newcomponent2 = SimpleComponent() s.appendComponent(newcomponent2) components = s.getComponentList() self.assert_(components == [newcomponent1, newcomponent2]) # Test prepending onto existing streams newcomponent3 = SimpleComponent() s.prependComponent(newcomponent3) components = s.getComponentList() self.assert_(components == [newcomponent3, newcomponent1, newcomponent2]) # Test prepending onto new streams s2 = stream.Stream() s2.prependComponent(newcomponent3) components = s2.getComponentList() self.assert_(components == [newcomponent3]) s2.prependComponent(newcomponent2) components = s2.getComponentList() self.assert_(components == [newcomponent2, newcomponent3]) if __name__ == "__main__": unittest.main() --- NEW FILE: socketstreamTest.py --- """Tests the socket streams to make sure they work, for AF_INET and SOCK_STREAM connections. Other types of streams are untested. Testing is accomplished by connecting to a known webserver and asking for a known file with known contents. Obviously, if you are not online, this will not work. This test assumes you are (as do other network tests).""" import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) sys.path.append("..") else: sys.path.append("..") import stream import socketstream import time server = 'www.jerf.org' file = '/socket.test.html' endswith = 'This is a socket test.\n' # FIXME: Re-activate these class SocketStreamTest(object): """Tests socket streams.""" def testUnthreadedStream(self): """Streams: Unthreaded socket stream works""" input, output = socketstream.getSocketStreams(host=server) output.put("GET %s HTTP/1.1\nHost: %s\n\n" % (file, server)) # FIXME: Really shouldn't need to do this, the stream should # block automatically time.sleep(3) # 3 seconds should be enough to retreive roughly # 500 bytes input.poll() result = "".join(list(input)) self.assert_(result.endswith(endswith)) def testThreadedStream(self): """Streams: Threaded socket stream works""" input, output = socketstream.getSocketStreams(host=server, threadedSocket=1) output.put("GET %s HTTP/1.1\nHost: %s\n\n" % (file, server)) input.join() result = "".join(list(input)) self.assert_(result.endswith(endswith)) if __name__ == "__main__": unittest.main() |
Update of /cvsroot/ironlute/ironlute/streams In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/streams Added Files: __init__.py component.py outlinestream.py socketstream.py stream.py streamconstants.py utilcomponents.py xmlcomponent.py xmlobjects.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: socketstream.py --- """socketstream.py provides utilities for hooking streams to sockets, and provides a function getSocketStreams for creating a socket and attaching streams to it in one function call. """ import stream import socket import threading def getSocketStreams(family = socket.AF_INET, type_ = socket.SOCK_STREAM, host = "127.0.0.1", port = 80, threadedSocket = 0, instream = None): """getSocketStreams will either raise an exception on failure, or return (instream, outstream), two streams connected to a socket created with the given arguments. Args straightforward, except threadedSocket: If 0, the instream must be periodically passed control by some thread to poll the socket. If 1, the socket will be polled by its own stream. In that case, you should pass an 'instream' in for the socket reader to use, fully prepared to function, or you may lose part of the stream (which is probably really bad).""" s = socket.socket(family, type_) s.connect((host, port)) # Well, we must have succeeded. outstream = stream.Stream(passiveOutput = 0) outstream.addOutput(SocketOutput(s, outstream)) if instream is None: instream = stream.Stream() # insocket depends on the threadedSocket argument if threadedSocket: SocketReader(s, instream) return (instream, outstream) # Otherwise, create a stream that needs to be polled every so # often instream.poll = SocketPoller(s, instream) return (instream, outstream) class SocketOutput(object): """Writes the final output of the stream to a socket. Expects strings, and will convert anything sent to it to a string (so be wary of Unicode.) Assumes it is the only output of value and will terminate the stream if the sending fails.""" def __init__(self, sock, s): self.socket = sock self.stream = s def __call__(self, obj): obj = str(obj) total = len(obj) sent = 0 while total > sent: try: sent += self.socket.send(obj[sent:]) except: self.stream.endStream() return class SocketReader(object): """Threaded socket reader that will feed into the given stream.""" def __init__(self, sock, s): self.socket = sock self.stream = s self.thread = threading.Thread(target = self.thread) self.stream.join = self.join self.thread.start() def join(self): self.thread.join() def thread(self): while 1: try: input = self.socket.recv(4096) if not input: self.stream.endStream() return except: self.stream.endStream() return self.stream.put(input) class SocketPoller(object): """This provides a "poll" method on the stream object which will poll the socket in a non-blocking fashion. This is best done in a GUI event loop or something. Note this turns on non-blocking calls, which could affect send. May have to change that later.""" def __init__(self, sock, s): self.socket = sock self.stream = s self.socket.setblocking(0) def __call__(self): # Error 11 is "resource temporarily unavailable", which # basically means "you read it all and there isn't any more yet" try: data = self.socket.recv(4096) except socket.error, e: if e.args[0] == 11: return raise while data: self.stream.put(data) try: data = self.socket.recv(4096) except socket.error, e: if e.args[0] == 11: return raise --- NEW FILE: __init__.py --- """Sets up the Streams library.""" from stream import Stream from component import Component from streamconstants import * from utilcomponents import Threader, LineFeedNormalizer from xmlcomponent import ExpatXMLComponent, XMLToStringComponent from socketstream import getSocketStreams, SocketOutput, \ SocketReader, SocketPoller from outlinestream import UnknownNodeTypeError, OutlineStreamElement,\ NodeElement, LinkElement, NodeToElements, ElementsToNode import xmlobjects import tests --- NEW FILE: utilcomponents.py --- """Implements a selection of general-use components.""" from streamconstants import EndOfStream import component import threading import Queue import zlib class Threader(component.Component): """A Threader takes its input and shoves it in a Queue, and another thread will then read the Queue. This has the effect of shoving complex stream computation into another thread. It exposes the Queue option to limit how full the queue can get, to prevent run-away input. This is useful to stick at the beginning of streams for when computation may take much longer then input, and you want to get the input out of the way to avoid using resources like the network, or sticking at the end of a stream to send stream computation into another thread automatically. Threader will add a .join method to the stream, to wait until all stream threads are done, and add itself to a list called 'threads' on the stream (creating it if necessary) which 'join' will use. """ def __init__(self, s, maxsize = 0): self.stream = s component.Component.__init__(self) self.queue = Queue.Queue(maxsize) self.thread = threading.Thread(target = self.thread) if hasattr(self.stream, 'threads'): self.stream.threads.append(self.thread) else: self.stream.threads = [self.thread] self.stream.join = self.joiner self.thread.start() # Threader will add this to the Stream if it is used. def joiner(self): for thread in self.stream.threads: thread.join() def put(self, obj): self.queue.put(obj) def thread(self): """This is the thread the Queue will implement. It pulls things from the Queue until it sees EndOfStream, at which point it propogates the EndOfStream and terminates the thread.""" while 1: nextObj = self.queue.get() self.output(nextObj) if nextObj is EndOfStream: return unicodeType = type(u'') class LineFeedNormalizer(component.Component): """LineFeedNormalizer takes incoming linefeeds (r or n), and normalizes them all to be a single n. Assumes incoming strings or Unicode, and will output string or Unicode depending on the input.""" def __init__(self): component.Component.__init__(self) self.leftoverR = 0 self.leftoverRUnicode = 0 def put(self, obj): if obj is EndOfStream: if self.leftoverR: if self.leftoverRUnicode: self.output(u"\n") else: self.output("\n") self.output(obj) return if self.leftoverR: obj = '\r' + obj self.leftoverR = 0 # Special case: If the last char is '\r', it *may* be part of # a "\r\n", split across two return objects. Save it for # later. if obj[-1] == "\r": obj = obj[:-1] self.leftoverR = 1 self.leftoverRUnicode = type(obj) is unicodeType obj = obj.replace("\r\n", "\n") obj = obj.replace("\r", "\n") self.output(obj) class ZLibCompressor(component.Component): """A ZLibCompressor takes in string and spits out a compressed version of that string. Note the zlib library can only handle actual strings, not Unicode. If you pass in a "codec" parameter, ZLibCompressor will try to decode any incoming unicode. Otherwise, you need to make sure it is all strings, or the default codec will handle it.""" def __init__(self, codec = None, level = 6): """In addition to the codec parameter, you can pass a level through to zlib w/ the 'level' parameter.""" component.Component.__init__(self) self.codec = codec self.compressobj = zlib.compressobj(level) def put(self, obj): if obj is EndOfStream: compressed = self.compressobj.flush() self.output(compressed) self.output(obj) return if self.codec is not None: obj = obj.encode(self.codec) compressed = self.compressobj.compress(obj) self.output(compressed) class ZLibDecompressor(component.Component): """A ZLibDecompressor takes in a compressed string and spits out a decompressed version of that string. Note the zlib library can only handle actual strings, not Unicode. If you pass in a "codec" parameter, ZLibDecompressor will try to encode any incoming string as unicode. ZLibDecompressor can optionally take a parameter 'transparent', which if true will cause the decompressor to check if the incoming string is a ZLib compressed string, checking at lesat the first eight bytes. If it is not, then the component will just pass the objects through unchanged. Otherwise, invalid data will raise an error. Note this isn't really recommended since you could potentially find a stream that *looks* compressed to zlib, but isn't; the more data passed in the first chunk the less likely this is. This does not currently work.""" def __init__(self, codec = None, transparent = 0): """In addition to the codec parameter, you can pass a level through to zlib w/ the 'level' parameter.""" component.Component.__init__(self) self.codec = codec self.transparent = transparent self.passthrough = 0 self.seenData = 0 self.data = "" self.decompressobj = zlib.decompressobj() # Work around Python 2.3 bug that causes segfault: # import zlib # zlib.decompressobj().flush() # # doesn't trigger w/ # # d = zlib.decompressobj() # d.decompress("") # d.flush() self.decompressobj.decompress("") def put(self, obj): if obj is EndOfStream: if self.transparent and self.data: # If we're trying to be transparent but haven't # yet accumlated enough date to do the check, # force it now try: if len(self.data) < 7: raise ValueError self.decompressobj.decompress(self.data) except: if self.data: self.output(self.data) self.output(obj) return decompressed = self.decompressobj.flush() self.output(decompressed) self.output(obj) return # If we're trying to be transparent, see if we need to check # this data for decompressability, and if this is enough data, # do the check if self.transparent and not self.seenData: self.data += obj if len(self.data) > 7: # We have enough data to test try: zlib.decompress(self.data) # If this passes through, we'll continue our # decompression attempts except: # Decompression failure, become a passthrough self.passthrough = 1 self.data = '' # Either way, we've seen data now self.seenData = 1 else: # Not enough data to tell yet, bail for now return if self.passthrough: self.output(obj) return # Otherwise, actually try to decompress decompressed = self.decompressobj.decompress(obj) if self.codec is not None: decompressed = decompressed.decode(self.codec) self.output(decompressed) --- NEW FILE: outlinestream.py --- """Outline streams take an outline and convert it into a series of pieces that can later re-construct the outline in its original form. Outline streams can take an outline iterator in that will present each node in order, and send out a series of OutlinePieces, which can then be transformed appropriately for some purpose. One of the properties of these streams is that any given complete stream may be taken and randomly re-ordered, and if re-assembled MUST produce an equivalent outline.""" # FIXME: This should be wrapped behind a "try" that checks for # the prescence of the outline library, and not bind anything if # it's not present # FIRST, we must define the pieces that encapsulate what the # OutlineToStream component generates and the StreamToOutline # component produces. import sys if ".." not in sys.path: sys.path.append("..") import sets import outline import ilIndex import component from streamconstants import * # Constants used to determine element type NODE = 0 LINK = 1 class UnknownNodeTypeError(Exception): """An unknown node type was specified.""" class OutlineStreamElement(object): pass # We get to be verbose with this function name because it should # never be called by external users def createElementFromNode(node, isRoot = False): """Creates a NodeElement from a node. Not generally useful for external users.""" return NodeElement(node.id, node.classId, node.getData(), [link.id for link in node.data.outgoing], isRoot) class NodeElement(OutlineStreamElement): """A Node Element encapsulates everything you need to know to reconstruct a node object such that once the stream is completely reconstructed, the new node in its new outline document is .equivalentTo the old node in the old outline document.""" elementType = NODE def __init__(self, id, nodeType, data, children, isRoot = 0): """nodeType is the *name* of the node type, as given by the NodeFiler. dataSource is something. Children is a list of children, identified by node id.""" self.id = id self.nodeType = nodeType self.data = data self.children = children self.isRoot = isRoot def node(self): """Retrieves the node defined by this node element. It does not generate a node with a matching id. See documentation for why that is.""" # Create the node try: klass = ilIndex.nodes.getByClassId(self.nodeType) except KeyError: raise UnknownNodeTypeError("Can't create node for " "unknown type %s" % self.nodeType) node = klass(data = self.data) return node def __repr__(self): root = "" if self.isRoot: root = " (root)" return "<NodeElement #%s%s>" % (self.id, root) def __eq__(self, other): if self.__class__ is not other.__class__: return 0 return self.id == other.id and \ self.nodeType == other.nodeType and \ self.data == other.data and \ self.children == other.children def createElementFromLink(link): # FIXME: Handle other information when we get there return LinkElement(link.id, link.source.id, link.dest.id) class LinkElement(OutlineStreamElement): """A LinkElement encapuslates everything you need to know to reconstruct a link from an OutlineStream.""" elementType = LINK def __init__(self, id, source, dest, other = None): self.id = id self.source = source self.dest = dest self.other = other def __eq__(self, other): if self.__class__ is not other.__class__: return 0 return self.source == other.source and \ self.dest == other.dest and \ self.other == other.other def __repr__(self): return "<LinkElement #%s (%s to %s)>" % (self.id, self.source, self.dest) # Now, define the components which convert nodes to the stream and # back again class NodeToElements(component.Component): """Converts a node into a series of NodeElements.""" def __init__(self): component.Component.__init__(self) # Track links we've already sent out self.seenLinks = sets.Set() self.seenRoot = False def put(self, iterator): # Pass if this is the end of the stream if iterator is EndOfStream: self.output(iterator) return # if "iterator" is actually a node, create a # default iterator if isinstance(iterator, outline.BasicNode): rootHandle = iterator.getRootHandle() iterator = rootHandle.depthFirst() # The iterator we are sent gives us the nodes for nodeHandle in iterator: node = nodeHandle.node self.output(createElementFromNode(node, not self.seenRoot)) self.seenRoot = True # Output not-yet-seen links for link in node.data.outgoing: linkid = link.id if linkid not in self.seenLinks: self.output(createElementFromLink(link)) self.seenLinks.add(linkid) # My intuition says there's a generic "lazy assembly" routine lying in # wait in here, but I can't see it clearly yet. It'll probably be # simpler and more robust then this, though... #FIXME TOMMOROW: Two major issues: # * When to output what node # * Does it work? class ElementsToNode(component.Component): """Takes a series of NodeElements and converts them back into their corresponding nodes.""" # Basically, we have to "lazily" construct the nodes as we have # enough data to do so. To construct a node, we must see the # original node; we can fill in links later. # We assume the first node is the root node, unless a later node # is explicitly marked as "the root" # FIXME: Be sure to test that at the end of a proper # reconstruction, the "pending"s and "unresolved"s are # empty def __init__(self): component.Component.__init__(self) # Hard links to the nodes we're constructing: # id => node self.nodes = {} # The links we've not yet resolved # id => linkElement self.unresolvedLinks = {} # The links we know exist, but have not yet seen # in the stream: id => (node, childIndex) self.pendingLinks = {} # The nodes we know exist, but have not yet seen # in the stream: # node id => [(node and childIndex that needs dest)] self.pendingNodes = {} self.root = None def addNodeElement(self, element): """Processes a node element, creating the node and resolving what links it can.""" node = element.node() # Root defaults to the first node we see, unless one is # explicitly marked "root" if self.root is None or element.isRoot: self.root = node self.nodes[element.id] = node # Populate the children, filling in links where possible and # filling in blanks where necessary for index, child in enumerate(element.children): # the link we'll fill in as details become available link = outline.Link(node) node.data.outgoing.append(link) # If we haven't seen this link yet, we can't do # any more for this child try: linkElement = self.unresolvedLinks[child] except KeyError: # This is a "pending link" self.pendingLinks[child] = (node, index) continue dest = linkElement.dest # Have we seen the destination yet? If so, we can # fully attach the link if dest in self.nodes: link.dest = self.nodes[dest] del self.unresolvedLinks[child] else: # Otherwise, mark that when we see this link, # we need to finish this link self.pendingNodes.setdefault(linkElement.dest, []).append((node, index)) try: del self.unresolvedLinks[child] except KeyError: pass # Were we waiting on this node to complete some other link? try: for source, index in self.pendingNodes[element.id]: link = source.data.outgoing[index] link.dest = node # This link is no longer "pending", since it is # resolved try: del self.pendingLinks[link.id] except KeyError: pass del self.pendingNodes[element.id] except KeyError: # Didn't find element.id in self.pendingNodes pass def addLinkElement(self, linkElement): """Process a link element, creating or resolving the link as necessary.""" # Two cases: Either this is a pending link (we've seen the # source node), or it's not id = linkElement.id if id in self.pendingLinks: sourceNode, index = self.pendingLinks[id] del self.pendingLinks[id] # We've seen the source, do we have the dest? if linkElement.dest in self.nodes: # Yes, so connect the two nodes and return destNode = self.nodes[linkElement.dest] link = sourceNode.data.outgoing[index] link.dest = destNode return else: # We have not yet seen the dest node, so # remember we need to hook up this link when # we see it self.pendingNodes.setdefault(linkElement.dest, []).append((sourceNode, index)) else: # Haven't seen source node, just slap it into unresolvedLinks self.unresolvedLinks[id] = linkElement def put(self, element): # Link or node? if element is EndOfStream: self.output(self.root) elif element.elementType == NODE: self.addNodeElement(element) elif element.elementType == LINK: self.addLinkElement(element) else: raise TypeError("Can't assemble node from a %s, " "because ElementToNode doesn't " "understand that.") --- NEW FILE: streamconstants.py --- """Both stream.py and component.py need some of these constants, so define them here where both can use them without mutual dependencies.""" # Condition of the stream: STREAM_ALIVE = 0 # stream is still running STREAM_OVER = 1 # stream terminated normally STREAM_TIMEOUT = 2 # stream timed out STREAM_ERROR = 3 # Constant so the streams know where to start START = 0 # Exceptions class StreamException(Exception): """The base class for stream exceptions.""" class TimeoutException(StreamException): """Some stream operations hit a timeout and the stream has terminated as a result.""" class DataErrorException(StreamException): """Some part of the stream encountered a data error and could not recover.""" class IteratorAlreadyExistsError(StreamException): """An iterator for passive output already exists.""" class StreamOver(StopIteration, StreamException): """The stream is terminated. A subclass of StopIteration so the streams can be used as iterators. Also raised if you put something into a terminated stream.""" class NoRequestHandlerError(StreamException): """A request was posted, but nobody wanted to handle it.""" class NoMoreDataError(StreamException): """More data was requested, but there is none available and no prospect of it showing up with the current threading.""" class Marker(object): """This is a base class used as a Marker object in the streams. Markers are objects used by the streams to communicate amongst themselves, and are therefore 'out of band'; it is not possible to pass these in the stream itself unless you wrap them yourself, though this should not be necessary.""" class EndOfStream(Marker): """This marker represents the end of a stream. It's a singleton. You can pass the EndOfStream instance, or call the endStream() method of the relevant stream.""" # The exceptions to raise for stream conditions other then # STREAM_ALIVE conditionExceptions = {} conditionExceptions[STREAM_TIMEOUT] = TimeoutException conditionExceptions[STREAM_OVER] = StreamOver conditionExceptions[STREAM_ERROR] = StreamOver EndOfStream = EndOfStream() defaultTimeout = 10.0 def isData(obj): """Returns true if the given object is a piece of data, rather then an exception or a marker.""" return not isinstance(obj, Exception) and \ not isinstance(obj, Marker) --- NEW FILE: component.py --- """component.py provides standard components for streams.""" from __future__ import generators from streamconstants import * import weakref # This is apparently guarenteed to execute atomically, preventing the # need for locking (check this) def idGenerator(): i = 0 while 1: yield i i = i + 1 ids = idGenerator() class Component(object): """Component provides certain common services for components, namely, the ID, the Hash, and the Equality function, so they can be compared and used in hashes. Basically, every instantiated Component will be unique and equal only to itself. Also defines the .output method, which outputs something. A default 'reverse' is defined which raises NotImplementedError. A default 'canReverse' is defined which returns 0.""" def __init__(self): self.id = ids.next() def output(self, obj): self.stream.components[self].put(obj) def put(self, obj): """Default component just passes the object through.""" self.output(obj) def __hash__(self): return hash(self.id) def __eq__(self, other): return self.id == other.id def reverse(self): raise NotImplementedError() def canReverse(self): return 0 # FIXME: Really ought to abstract this # Make 'stream' a weak property, to prevent GC loops w/ streams # (I thought 2.3 took care of this case but it was still leaking # in 2.3.3 on my Linux) def getStream(self): return self._stream() def setStream(self, s): self._stream = weakref.ref(s) stream = property(getStream, setStream) class StreamOutput(Component): """The StreamOutput component interfaces between the components and the stream output mechanisms. It is automatically instantiated by the Streams. If you ever think you want to modify this, what you REALLY want is to derive a new stream and override the .output method.""" def put(self, obj): self.stream.output(obj) --- NEW FILE: xmlcomponent.py --- """xmlcomponent uses Expat to convert a stream of strings into a stream of XML events. An object will be sent down the stream for each XML event Expat produces.""" import component import xml.parsers.expat from xmlobjects import * import streamconstants class ExpatXMLComponent(component.Component): """The ExpatXMLComponent exposes the Expat parser. It will output various XML objects when the Expat parser produces something, as defined in xmlobjects. (xmlobjects.py is seperate so other parsers may be slotted in later, without changing other things.) The ExpatXMLParser expects strings in (as in, the original binary of the file), and will output either Unicode strings, or strings encoded in UTF-8, as controlled by the second parameter to this function. If true (default), Unicode strings will be output..""" def __init__(self, unicode = 1): component.Component.__init__(self) # Create the parser and set it up. p = self.parser = xml.parsers.expat.ParserCreate(None, " ") p.XmlDeclHandler = self.xml_decl p.StartElementHandler = self.start_element p.EndElementHandler = self.end_element p.CharacterDataHandler = self.chars p.StartDoctypeDeclHandler = self.doctype p.EndDoctypeDeclHandler = self.end_doctype p.ProcessingInstructionHandler = self.pi_handler p.StartNamespaceDeclHandler = self.start_namespace_decl p.EndNamespaceDeclHandler = self.end_namespace_decl p.CommentHandler = self.comment p.StartCdataSectionHandler = self.start_cdata p.EndCdataSectionHandler = self.end_cdata self.parser.returns_unicode = unicode def put(self, obj): if obj is streamconstants.EndOfStream: self.output(obj) return try: self.parser.Parse(obj) except xml.parsers.expat.ExpatError, e: self.output(XMLError(e)) self.stream.status = streamconstants.STREAM_ERROR raise except Exception: self.stream.status = streamconstants.STREAM_ERROR raise def xml_decl(self, *args): self.output(XMLDeclaration(*args)) def start_element(self, *args): self.output(StartElement(*args)) def end_element(self, *args): self.output(EndElement(*args)) def chars(self, *args): self.output(CharacterData(*args)) def doctype(self, *args): self.output(StartDocType(*args)) def end_doctype(self, *args): self.output(EndDocType(*args)) def pi_handler(self, *args): self.output(ProcessingInstruction(*args)) def start_namespace_decl(self, *args): self.output(StartNamespaceDecl(*args)) def end_namespace_decl(self, *args): self.output(EndNamespaceDecl(*args)) def comment(self, *args): self.output(Comment(*args)) def start_cdata(self, *args): self.output(StartCData(*args)) def end_cdata(self, *args): self.output(EndCData(*args)) class XMLToStringComponent(component.Component): """Takes incoming XMLObjects and converts them to str (octets).""" def __init__(self, autoCreateMnemonics = 1, verifyTags = 0): component.Component.__init__(self) self.context = XMLContext(autoCreateMnemonics = autoCreateMnemonics, verifyTags = verifyTags) def put(self, obj): if isinstance(obj, XMLBase): # Fix up any context-sensitive XML issues self.context.present(obj) self.output(obj.xml()) --- NEW FILE: xmlobjects.py --- """xmlobjects defines a series of XML Objects the xml->xmlobject components can define.""" import streamconstants from xml.sax.saxutils import escape, quoteattr ENCODING = "utf-8" #FIXME: inCDATA needs to be moved to the XMLContext def createName(mnemonic, name): """Constructs the name from the mneomonic and the name; encapsulates the knowlege that the default namespace (which is the empty string according to the spec) does not get a colon in front of it.""" if mnemonic: return "%s:%s" % (mnemonic, name) else: return name def getNewMnemonic(namespaces): """Assuming that namespaces is a dict containing the mnemonics as keys, goes looking for a simple new mnemonic that can be allocated for a new namespace. It's English-centric, but this should be a last resort anyhow and really should never be seen in 'real' life.""" # Try single letters: alphabet = "abcdefghijklmnopqrstuvwxyz" for char in alphabet: if char not in namespaces: return char # Give it a numeric affix i = 1 while 1: for char in alphabet: if char + str(i) not in namespaces: return char + str(i) i += i # Will eventually terminate... if your XML is so pathological that # this takes an appreciable time to run... heck, if it's so # pathological that you even make it to numeric suffixes!... then # you probably have other, bigger problems class XMLError(streamconstants.DataErrorException): """The stream encountered a data error and that error was some sort of outgoing XML error; see subclasses.""" class WrongClosingTagError(XMLError): """The closing tag sent out the XML stream does not match the tag it should be closing (i.e., <a></b>).""" class NoNamespaceError(XMLError): """The namespace used in the tag's name does not have a declared mnemonic, and auto-mnemonics are turned off.""" class XMLContext(object): """XMLContext tracks an outgoing XML objects stream and modifies the stream to add certain features to it. All StartTag, EndTag, and StartNamespaceDecl tags MUST be presented to this context for it to work correctly. For future compatibility, it is probably a good idea for ALL of them to be passed in. 1. Correct namespace handling; sending in xmlns attributes or sending StartNamespaceDecl objects will work correctly. 2. Tracks the tag stack and optionally verifies it is correct. 3. Can either auto-create mnemonics (albeit most likely human-meaningless ones) as needed, or raise an error if a non-declared namespace is used. By default, the most permissive settings are used: autoCreateMnemonics is set to true and verifyTags to false.""" def __init__(self, autoCreateMnemonics = 1, verifyTags = 0): """autoCreateMnemonics: If true (default), the context will create mnemonics as needed, instead of throwing NoNamespace exceptions. verifyTags: If true (default false), throws exception if the end tags are presented out of order (i.e., <a></b>).""" # url => mnemonic self.namespaces = {} # mnemonic => url stack self.mnemonics = {} self.nsNextTag = [] self.tagStack = [] self.addedNamespaces = {} self.addedMnemonics = {} self.autoCreateMnemonics = autoCreateMnemonics self.verifyTags = verifyTags # Are we in the middle of a CDATA section? Some of # the XML objects need to know not to escape their # contents if so self.inCDATA = 0 def pushTag(self, tag): self.tagStack.append(tag) def addNamespace(self, mnemonic, namespace): self.namespaces.setdefault(namespace, []).append(mnemonic) self.mnemonics.setdefault(mnemonic, []).append(namespace) # Record the added namespaces # (the present method pushes the tag before calling this # method) tag = self.tagStack[-1] self.addedNamespaces.setdefault(tag, []).append(namespace) self.addedMnemonics.setdefault(tag, []).append(mnemonic) # in a way, the tagStack here is a mini-class based on a list, # encapsulated by the pushTag and popTag methods. def popTag(self): """Pops the last tag off of the tag stack and manages the namespace mnemonics.""" tag = self.tagStack.pop() if tag in self.addedNamespaces: for ns in self.addedNamespaces[tag]: self.namespaces[ns].pop() if tag in self.addedMnemonics: for mnemonic in self.addedMnemonics[tag]: self.mnemonics[mnemonic].pop() return tag def namespaceHasMnemonic(self, ns): """Verifies that the given namespace has a current mnemonic.""" # In order to be certain that the namespace is correct, you # have to look up the latest mnemonic for the given namespace, # THEN verify that it is up to date for the given # mnemonic. See test suite for the crazy cases where this matters. if ns not in self.namespaces: return 0 # The latest mnemonic we know mnemonic = self.namespaces[ns][-1] # Is it still up to date? if self.mnemonics[mnemonic][-1] != ns: # Nope, something overwrote this mnemonic return 0 else: # Yep, it matches return 1 def validateNamespace(self, namespace): """Private. Validates that the namespace is valid, and either creates a new one, or raises an exception. Either returns or raises.""" if not self.namespaceHasMnemonic(namespace): if not self.autoCreateMnemonics: # Let's go ahead and compute the error for the # user if namespace not in self.namespaces: raise NoNamespaceError("Namespace %s " "has no mnemonic." % namespace) else: mnemonic = self.namespaces[namespace][-1] data = (namespace, mnemonic, self.mnemonics[mnemonic][-1]) raise NoNamespaceError("Namespace %s " "has had mnemonic " "%s obscured by " "%s." % data) # Otherwise, it's time to make a new namespace else: newMnemonic = getNewMnemonic(self.mnemonics) self.addNamespace(newMnemonic, namespace) return def present(self, obj): if not isinstance(obj, XMLBase): return if isinstance(obj, StartElement): self.tagStack.append(obj) atts = obj.attributes # Look for xmlns:x="ns" attributes for name, val in atts.iteritems(): if name[:5] == "xmlns": self.addNamespace(name[6:], val) # See if there's any namespaces to add from previous # StartNamespaceDecl objects for mnemonic, namespace in self.nsNextTag: self.addNamespace(mnemonic, namespace) if mnemonic: obj.attributes['xmlns:%s' % mnemonic] = namespace else: obj.attributes['xmlns'] = namespace self.nsNextTag = [] # Now, process both the name and attributes, converting # them to the appropriate mnemonics as necessary. if ' ' in obj.name: # *definately* an error to have multiple spaces try: namespace, name = obj.name.split(" ") except ValueError: print "name: %s" % obj.name raise self.validateNamespace(namespace) namespace = self.namespaces[namespace][-1] obj.name = createName(namespace, name) # need ".keys()" so we can modify dict in loop for attname in atts.keys(): if ' ' in attname: namespace, name = attname.split(' ') self.validateNamespace(namespace) namespace = self.namespaces[namespace][-1] newattname = createName(namespace, name) atts[newattname] = atts[attname] del atts[attname] return if isinstance(obj, EndElement): name = obj.name # Convert name to mnemonic name to match start tag startTag = self.popTag() if self.verifyTags and name != startTag.name: raise WrongClosingTagError("End element %s does " "not match start element " "%s." % (name, startTag.name)) # Match the name the start tag used, to match the # mnemonics. obj.name = startTag.name return if isinstance(obj, StartCData): self.inCDATA = 1 return if isinstance(obj, EndCData): self.inCDATA = 0 return if isinstance(obj, CharacterData): # Tell the character data it shouldn't encode, because # it's in a CDATA section obj.inCDATA = 1 if isinstance(obj, StartNamespaceDecl): self.nsNextTag.append((obj.prefix, obj.uri)) return class XMLBase(object): """Defines an __init__ which will automatically assign things to the class correctly.""" def __init__(self, *args): for thing in zip(self.args, args): setattr(self, thing[0], thing[1]) # Set names to strings if self.args and self.args[0] == 'name': self.name = unicode(self.name) def sameAs(self, other): if type(self) is not type(other): return 0 for arg in self.args: if getattr(self, arg) != getattr(other, arg): return 0 return 1 def __repr__(self): args = ['%s="%s"' % (x, getattr(self, x)) for x in self.args] return "<%s %s>" % (self.__class__.__name__, ' '.join(args)) def __eq__(self, other): return self is other def xml(self): """Return the XML version of this object.""" raise NotImplementedError class XMLError(streamconstants.DataErrorException): """Records the error that happens. FIXME: Ought to make this non-parser-specific.""" def __init__(self, error): self.originalError = error class XMLDeclaration(XMLBase): """The XMLDeclaration is the <?xml version='1.0'?> at the beginning of the document.""" args = ('version', 'encoding', 'standalone') def xml(self): s = ['<?xml'] # version is required in XML s.append('version="' + self.version + '"') if self.encoding is not None: s.append('encoding="' + self.encoding + '"') if self.standalone is not None and self.standalone is not -1: if self.standalone: s.append('standalone="yes"') else: s.append('standalone="no"') s.append('?>\n') return ' '.join(s) class StartDocType(XMLBase): """The <!DOCTYPE ...""" args = ('doctypeName', 'systemId', 'publicId', 'has_internal_subset') class EndDocType(XMLBase): args = () class StartElement(XMLBase): """The beginning of a tag, with the attributes as a name->value dictionary.""" args = ('name', 'attributes') def xml(self): s = ["<" + self.name] if hasattr(self, "attributes"): for name, value in self.attributes.iteritems(): s.append(name + '=' + quoteattr(value)) return octets(' '.join(s)) + ">" class EndElement(XMLBase): """The end of a tag.""" args = ('name',) def xml(self): return "</" + octets(self.name) + ">" class StartNamespaceDecl(XMLBase): args = ('prefix', 'uri') def __init__(self, *args, **kwargs): XMLBase.__init__(self, *args, **kwargs) if self.prefix is None: self.prefix = '' def xml(self): """Affects the next element. The XMLContext handles this, so this is just the empty string.""" return '' class EndNamespaceDecl(XMLBase): args = ('prefix',) def xml(self): return '' class Comment(XMLBase): args = ('data',) def xml(self): return "<!--" + octets(self.data) + "-->" class StartCData(XMLBase): args = () def xml(self): return "<![CDATA[" class EndCData(XMLBase): args = () def xml(self): return "]]>" class CharacterData(XMLBase): args = ('data',) def xml(self): if hasattr(self, 'inCDATA') and \ self.inCDATA: return octets(self.data) else: return octets(escape(self.data)) class ProcessingInstruction(XMLBase): args = ('target', 'data') def xml(self): return "<?%s %s>" % (octets(self.target), octets(self.data)) def octets(what, encoding=ENCODING): """Takes any string and converts it to a string of octets. A string is considered here to be a series of octets suitable for transmission, not text.""" if type(what) == type(''): return what return what.encode(ENCODING) --- NEW FILE: stream.py --- """Streams are an abstraction for flowing data from one point to another, possibly with transformations along the way. Streams are designed to be very flexible, multi-threading aware (i.e., it is possible, though not required, to use streams in a multi-threading safe way), and easy-to-use. Streams consist of a series of Components (see component.py) which tell the stream how to process the data stream. The stream accepts input of arbitrary Python objects via the .put method. The stream will call any number of Python functions for output information. Alternatively (and exclusively to the above), the Stream may be accessed via a Python iterator. Alternatively to the .put method, a stream may suck its input out of a provided Python iterator. Inside a stream, only one path may execute. A stream may output to several locations, though. Several useful adapters are provided to provide multi-threaded safety. By default, streams are not multi-threading safe and always execute in the calling thread. A component is provided that will send its input into another thread. An output adapter is provided that will do the same. REQUESTS -------- The other major feature a stream provides is the ability to make requests. which are limited forms of communication going upstream. They take the form of StreamRequest objects. Documentation and examples later. """ from __future__ import generators import component import threading import Queue import time from streamconstants import * # FIXME: Research turning the EndOfStream event into a seperate # method; having it in Put is confusion. (Metaclass wrapping put?) # Remember there may be flexibility advantages in treating it # normally, see if we can't get best of both worlds. class Stream(object): """A Stream is the controlling object for streams. Generally, this stream will be sufficient and its behavior can be changed via composition, as discussed in the __init__ function doc string. Convenience functions should be provided for certain streams with common component loads. If anything .put s an EndOfStream token, or outputs an EndOfStream token, the stream will consider itself terminated and no longer accept input.""" def __init__(self, output = None, passiveOutput = None): """Parameters: * output: If given, will be used by the stream as the initial destination method. Should be callable. * passiveOutput: If this is true, a PassiveOutput instance will be added to this stream, and an '__iter__' method will be added to this stream object which is delegated to that PassiveOutput object. This defaults to True if no output is given, and False if it is. """ self.outputs = [] if output is not None: self.addOutput(output) self.components = {} self.components[START] = component.StreamOutput() self.components[START].stream = self self.endComponent = START # marks the end, so we can append if passiveOutput is None: if output is None: passiveOutput = 1 if passiveOutput: po = PassiveOutput() self.addOutput(po) self.passiveOutput = passiveOutput self.status = STREAM_ALIVE def addOutput(self, output): self.outputs.append(output) def appendComponent(self, component): """appendComponent(component) - adds a component to the end of the stream.""" # We actually want to preserve the StreamOutput on the end, so # this appends one before the end first = self.endComponent last = self.components[first] # first now goes to the new component, and the new component # goes to last, which is the StreamOutput component self.components[first] = component self.components[component] = last component.stream = self self.endComponent = component def prependComponent(self, component): """prependComponent(component) - prepends a component to the beginning of the stream""" componentCount = len(self.components) self.components[component] = self.components[START] self.components[START] = component component.stream = self # Special case: If we're prepending onto the beginning of a # new stream, this is the new 'endComponent' if componentCount == 1: self.endComponent = component def getComponentList(self): """private - returns components as a list. Useful for testing. Doesn't show the output component on the end.""" components = [] c = 0 last = self.components[self.endComponent] while c is not self.endComponent: c = self.components[c] components.append(c) return components def multiPut(self, iterable, endStream = 1): """multiPut(iterable, endStream = 1) - pulls from iterable until the iterator terminates. If endStream is true, also puts an EndOfStream into the stream.""" for obj in iterable: self.put(obj) if endStream: self.endStream() def put(self, obj): """put(obj) - insert an object into the stream""" if self.status != STREAM_ALIVE: raise conditionExceptions[self.status]() if obj is EndOfStream: self.status = STREAM_OVER self.components[START].put(obj) def output(self, obj): """output(obj) - distributes 'obj' to all registered output mechanisms. called by the StreamOutput component and nobody else.""" if obj is EndOfStream: self.status = STREAM_OVER for outputter in self.outputs: try: outputter(obj) except: pass def endStream(self): self.put(EndOfStream) def __iter__(self): if not self.passiveOutput: return emptyIterator() else: return iter(self.outputs[0]) # FIXME: Rely on this at your own mortal peril. I'm not convinced # this works correctly. def __del__(self): """If the stream has not yet been explicitly terminated, end the stream, so things like the Threader component will terminate properly. After that, normal garbage collection is safe.""" try: if self.status == STREAM_ALIVE: self.endStream() except: pass class PassiveOutput(object): """PassiveOutput collects everything it is called with. You can get the next item with the 'get' method, or use the __iter__ method. This is only really suitable in a single-threaded environment where these methods will be called when the stream has exhausted itself, because this object has no way to know whether any more input is coming, and will terminate either when there is no more data or when it sees EndOfStream, whichever comes first.""" def __init__(self): self.incounter = 0 self.outcounter = 0 self.data = {} def get(self): if self.outcounter not in self.data: raise NoMoreDataError("There is no more data in this " "stream.") data = self.data[self.outcounter] self.outcounter += 1 return data def __call__(self, obj): self.data[self.incounter] = obj self.incounter += 1 def __iter__(self): return self.outputIter() def outputIter(self): while 1: try: data = self.get() except NoMoreDataError: raise StreamOver if data is EndOfStream: raise StreamOver yield data def emptyIterator(): # see http://groups.google.com/groups?selm=ANLA9.10384%24Yw.422374%40news2.tin.it yield iter([]).next() # This is implemented as a class so the streams have something to pass # around, but is used as a function class StreamRequest(object): def __new__(cls, stream, name, args = None): self = object.__new__(cls) self.stream = stream self.name = name self.args = args return self.stream.postRequest(self) |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:11
|
Update of /cvsroot/ironlute/ironlute/simpleFile/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/simpleFile/tests Added Files: fileTest.py httpTest.py manilaTest.py simpleFileTest.py stringTest.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: httpTest.py --- """http testing: This testing file requires the user to be online, which isn't usually TOO onerous...""" # FIXME: Commit to a constant testing page somewhere import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) sys.path.append("..") else: sys.path.append("..") import sfile import http class TestHTTP(unittest.TestCase): def test_basic_reading(self): """SimpleFile HTTP: Can read HTTP""" f = http.HTTPFile("http://www.jerf.org/") self.assert_(f.read()) def test_reading_by_chunks(self): """SimpleFile HTTP: Can read by chunks""" f = http.HTTPFile("http://www.jerf.org/") content = f.read() chunks = [] c = f.readChunk(1024) while c: chunks.append(c) c = f.readChunk(1024) self.assert_(''.join(chunks) == content) def test_failures(self): """SimpleFile HTTP: Fails correctly""" f = http.HTTPFile("http://www.jerf.org/") self.assertRaises(sfile.CantDeleteError, f.delete) self.assertRaises(sfile.CantRenameError, f.rename, 'anything') if __name__ == "__main__": unittest.main() --- NEW FILE: fileTest.py --- """Test the File implementation, also testing the shell on 'real' code.""" import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) sys.path.append("..") else: sys.path.append("..") import sfile File = sfile.File # Note security flaw here; run this test only on trusted systems # patches with better *cross-platform* logic gratefully accepted _tmpFileName = None # Avoid extraneous warnings while being scanned by moduleIndexer def getTmpFileName(): global _tmpFileName if _tmpFileName is not None: return _tmpFileName _tmpFileName = os.tmpnam() return _tmpFileName # FIXME: Redundency in this and String; there's a generalized tester # here trying to get out testdata = "abcdefg1234567" # Use \n to force LF testdataLinesNoLFEnd = "abcdefg\n1234567\nabcdefg\n1234567" testdataLinesLFEnd = "abcdefg\n1234567\nabcdefg\n1234567\n" class TestFile(unittest.TestCase): def setUp(self): # always start by clearing the temp file if it exists try: os.remove(getTmpFileName()) except OSError: pass def tearDown(self): # always end that way, too try: os.remove(getTmpFileName()) except OSError: pass def testBasicWriting(self): """SimpleFile File: basic reading/writing works""" f = File(getTmpFileName()) # test direct writing f.writeAll(testdata) data = f.read() self.assert_(data == testdata) # test writing in chunks, despite the fact strings # do not directly support it for char in testdata: f.write(char) data = f.read() self.assert_(data == testdata) f.close() f.write("abc") f.write("abc") self.assert_(f.read() == "abcabc") def test_chunked_reading(self): """SimpleFile File: chunked reading""" f = File(getTmpFileName()) f.writeAll(testdata) data = [] c = f.read(3) while c: data.append(c) c = f.read(3) self.assert_(data == ['abc', 'def', 'g12', '345', '67']) def test_read_lines(self): """SimpleFile String: test read/write lines""" f = File(getTmpFileName()) for transform in ['\n', '\r', '\r\n']: f.lineTerminator = transform f.writeAll(testdataLinesNoLFEnd.replace('\n',transform)) lines = list(f.readLines()) self.assert_(lines == ['abcdefg', '1234567', 'abcdefg', '1234567']) f.write(testdataLinesLFEnd.replace('\n', transform)) lines = list(f.readLines()) self.assert_(lines == ['abcdefg', '1234567', 'abcdefg', '1234567', '']) if __name__ == "__main__": unittest.main() --- NEW FILE: stringTest.py --- """Simple file testing the string, mostly to make sure simpleFile shell works.""" import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) sys.path.append("..") else: sys.path.append("..") import sfile testdata = "abcdefg1234567" # Use \n to force LF testdataLinesNoLFEnd = "abcdefg\n1234567\nabcdefg\n1234567" testdataLinesLFEnd = "abcdefg\n1234567\nabcdefg\n1234567\n" class TestSimpleFileString(unittest.TestCase): def test_basic_writing(self): """SimpleFile String: basic reading/writing works""" f = sfile.StringFile() # test direct writing f.writeAll(testdata) data = f.read() self.assert_(data == testdata) # test writing in chunks, despite the fact strings # do not directly support it for char in testdata: f.write(char) data = f.read() self.assert_(data == testdata) f.close() f.write("abc") f.write("abc") self.assert_(f.read() == "abcabc") def test_chunked_reading(self): """SimpleFile String: chunked reading""" f = sfile.StringFile() f.writeAll(testdata) data = [] c = f.read(3) while c: data.append(c) c = f.read(3) self.assert_(data == ['abc', 'def', 'g12', '345', '67']) def test_read_lines(self): """SimpleFile String: test read/write lines""" f = sfile.StringFile() for transform in ['\n', '\r', '\r\n']: f.lineTerminator = transform f.writeAll(testdataLinesNoLFEnd.replace('\n',transform)) lines = list(f.readLines()) self.assert_(lines == ['abcdefg', '1234567', 'abcdefg', '1234567']) f.write(testdataLinesLFEnd.replace('\n', transform)) lines = list(f.readLines()) self.assert_(lines == ['abcdefg', '1234567', 'abcdefg', '1234567', '']) def test_some_failures(self): """SimpleFile String: Assert correct failures occur""" f = sfile.StringFile() self.assert_(f.read() == '') self.assertRaises(sfile.CantRenameError, f.rename, 'anything') self.assertRaises(sfile.CantDeleteError, f.delete) if __name__ == "__main__": unittest.main() --- NEW FILE: manilaTest.py --- """Manila testing... tests that we can read Manila messages. This is dependent on the exact message you're reading to test, so you'll need to modify this file to fit something you can download, OR set the 'dontTest' value to 1 so it always automatically passes. WARNING: This code could potentially *destroy* the message you use to test with, if the writing only partially works!""" dontTest = 0 import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) sys.path.append("..") else: sys.path.append("..") import sfile import manila import userDirectory import warnings directory = userDirectory.makeAppDirectory("Iron Lute") filename = os.path.join(directory, "testdata", "manilaPasswd") try: username, password = \ sfile.File(filename).read().split(',') password = password.replace('\n', '') manilaHost = "irights.editthispage.com" manilaMessage = 1333 testdata = "This is some test data." class TestManila(unittest.TestCase): def testManila(self): """SimpleFile Manila: Works?""" f = manila.ManilaMessage(manilaHost, username, password, manilaMessage) original = f.read() self.assert_(original) # try to make sure it's working f.writeAll(testdata) new = f.read() self.assert_(new == testdata) f.writeAll(original) new = f.read() self.assert_(new == original) except IOError: class TestManila(unittest.TestCase): def testManila(self): warnings.warn("Could not run Manila tests since no test site" " is configured.") if __name__ == "__main__": unittest.main() --- NEW FILE: simpleFileTest.py --- """Test the SimpleFile shell, mostly the parameter handling since the shell itself does little.""" import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) sys.path.append("..") else: sys.path.append("..") import sfile class TestString(sfile.StringFile): """A test string-based class for parameter passing.""" baseCapabilities = [sfile.CAP_READ, sfile.CAP_WRITE] group = "Test" name = "Test" description = "A test simpleFile." paramList = [ ["Test Param 1", "The first test param", "test1", 'schmoo'], ["Test Param 2", "The second test param", "test2"] ] class TestSimpleFile(unittest.TestCase): def test_various_parameters(self): """SimpleFile: Do parameters work right?""" test1 = TestString() self.assert_(test1.test1 == 'schmoo') self.assert_(test1.test2 is None) test2 = TestString(test2 = 'hut') self.assert_(test2.test1 == 'schmoo') self.assert_(test2.test2 == 'hut') test3 = TestString('argh') self.assert_(test3.test1 == 'argh') self.assert_(test3.test2 is None) test4 = TestString('foo', 'bar') self.assert_(test4.test1 == 'foo') self.assert_(test4.test2 == 'bar') self.assert_(test4.getParamList() == ['foo', 'bar']) test5 = TestString(*test4.getParamList()) self.assert_(test4.getParamList() == test5.getParamList()) self.assert_(test4.getParamDict() == {'test1': 'foo', 'test2': 'bar'}) if __name__ == "__main__": unittest.main() |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:11
|
Update of /cvsroot/ironlute/ironlute/filetypes/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/filetypes/tests Added Files: genericTextTest.py ilformatTest.py structuredTextTest.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: structuredTextTest.py --- """Tests the StructuredText filetype and auxilary functions. If run directly, it runs all the tests. It can be imported, and you can use the getTests() function to return a list of instantiated TestCase objects.""" import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: if ".." not in sys.path: sys.path.append("..") if "../.." not in sys.path: sys.path.append("../..") import structuredText as st import exceptions import outline class indentLevelTest(unittest.TestCase): """Does the indent function work correctly?""" cases = [ (0, "Hello."), (1, " Hello"), (2, " Hi!"), (0, " Hi!\nThere!"), (1, " Hi!\n There!"), (0, "\n Yo!\nThere."), (2, " Hello!\n") ] def testGoodness(self): """structuredText: indentLevel is correct""" for case in self.cases: self.failUnlessEqual(st.indentLevel(case[1]), case[0], "'%s' showed %i, not %i" % (case[1], st.indentLevel(case[1]), case[0])) def testFailure(self): """structuredText: indentLevel accepts only strings""" self.assertRaises(exceptions.TypeError, st.indentLevel, 1) class extractTextTest(unittest.TestCase): """Does the extract text function work correctly?""" cases = [ ("", ""), ("Hello!", "Hello!"), ("Hello!\n", "Hello!"), (" Hi, this is a test.\n ", "Hi, this is a test."), (" Hi, this is a test.\nOf love.", "Hi, this is a test. Of love.") ] def testGoodness(self): """structuredText: extractText works correctly""" for case in self.cases: self.failUnlessEqual(st.extractText(case[0]), case[1]) def testFailure(self): """structuredText: extractText accepts only strings""" self.assertRaises(exceptions.TypeError, st.extractText, 1) class normalizeLineFeedsTest(unittest.TestCase): """Does the line feed normalizer work?""" cases = [ ("", ""), ("Hello!", "Hello!"), ("\n", "\n"), ("\r", "\n"), ("\r\n", "\n"), ("\r\n\r\n", "\n\n"), ("\r\r", "\n\n"), ("\n\n", "\n\n"), ("\n\r\n", "\n\n"), # bad idea, but it's well defined ("\r\n \r\n!!", "\n \n!!") ] def testGoodness(self): """structuredText: normalizeLineFeeds works""" for case in self.cases: a, b = case a = st.normalizeLineFeeds(a) self.failUnlessEqual(a,b, "case %s, %s" % (repr(case[0]), repr(case[1]))) class structuredTextReaderTest(unittest.TestCase): """Does the reader work correctly?""" def testGoodness(self): """structuredText: structuredTextReader works""" reader = st.StructuredTextReader flat = outline.BasicDocument() flat.addNewChild(childArgs=("Hello one.",)) flat.addNewChild(childArgs=("Hello two.",)) flat.addNewChild(childArgs=("Hello three.",)) indented = outline.BasicDocument() indented.addNewChild(childArgs=("Hello one.",)) indented[0].addNewChild(childArgs=("Hello two.",)) indented[0].addNewChild(childArgs=("Hello three.",)) indented.addNewChild(childArgs=("Hello four.",)) small = outline.BasicDocument() small.addNewChild(childArgs=("Hello one.",)) small.addNewChild(childArgs=("Hello two.",)) s1 = reader(st1).outlineNodes() s2 = reader(st2).outlineNodes() s3 = reader(st3).outlineNodes() s4 = reader(st4).outlineNodes() s5 = reader(st5).outlineNodes() s6 = reader(st6).outlineNodes() self.assert_(s1.equivalentTo(flat)) self.assert_(s2.equivalentTo(indented)) self.assert_(s3.equivalentTo(indented)) self.assert_(s4.equivalentTo(small)) self.assert_(s2.equivalentTo(s3)) self.assert_(s5.equivalentTo(s1)) # Test the more complicated st6 qmn = outline.quickMakeNodes complicated = qmn([["Indent 0.", ['Indent 1.', 'Indent 2.'], ['Indent 1.', 'Indent 2.']]]) self.assert_(s6.equivalentTo(complicated)) class structuredTextSanityTest(unittest.TestCase): """For test data contained in the strings below this, does the string->reader->outlineNodes->writer->str->reader->outlineNodes produces two equal sets of outline nodes?""" def getTheNodes(self, s): r1 = st.StructuredTextReader(s) nodes1 = r1.outlineNodes() w1 = st.StructuredTextWriter(nodes1) s2 = w1.fileString() r2 = st.StructuredTextReader(s) nodes2 = r2.outlineNodes() return nodes1, nodes2 def testSanity(self): """structuredText: read == write->read (sanity)""" strList = [st1, st2, st3, st4, st5, testData] for s in strList: n1, n2 = self.getTheNodes(s) self.assert_(n1.equivalentTo(n2)) self.assert_(n2.equivalentTo(n1)) class JerfTextTest(unittest.TestCase): """Does Jerf's little hack work correctly?""" def testJerfRead(self): """JerfTextReader: Basic Functioning""" j = st.JerfTextReader(JerfText) nodes = j.outlineNodes() self.assert_(nodes.getData('title') == "A Title") rootHandle = nodes.getRootHandle() for childHandle in rootHandle.depthFirst(suppressRoot = True, yieldMarkers = False): child = childHandle.getNode() self.assert_(child.getData() == child.__class__.__name__, "Got text of %s but class of %s" % (child.getData(), child.__class__.__name__)) self.assert_(nodes[3].level == 2) JerfText = """!title A Title HeaderNode BasicNode * UnorderedListElementNode )QuoteNode * UnorderedListElementNode && HeaderNode % PreformattedNode HeaderNode BasicNode ) QuoteNode #OrderedListElementNode ) QuoteNode """ st1 = """Hello one. Hello two. Hello three.""" st2 = """Hello one. Hello two. Hello three. Hello four.""" st3 = """ Hello one. Hello two. Hello three. Hello four. """ st4 = """ Hello one. Hello two.""" st5 = """\nHello one. Hello two. Hello three.""" st6 = """Indent 0. Indent 1. Indent 2. Indent 1. Indent 2. """ testData = """I used to think of this as a ThoughtSpace, but that's too static-sounding. Hmmm, does that mean I need to change my planned product name of ThoughtFlux? There is this public-readable space, plus a completely private space. Sometimes I think it would make sense to combine them, but probably not. Though I may want an easy way to copy/move/clone a page from one space to the other. I'm also interested in collaborative/shared spaces. But my current beliefs are: * these need a tangible goal/outcome ("when are we done?") * they probably need to have a controlled population (or some process of rapidly building/maintaining trust) So I'm not trying to handle such a thing right now. **So what are my needs?** my individual public writing space * content types * blog bits (short items) (need to be able to be multiple paragraphs). Before I consider dropping my current UserlandManila site, I'd better think hard about how much of ReplicatingUserland is necessary. * longer "articles": use WiKi pages for this (since it provides the easiest HyperText creation approach). Would it be OK to have separate wiki space, and just write blog bits to promote them? But then don't get other features like GroupDiscussion tied to them (though I suppose you post discussions to the blog bit...) (or does CMFWiki do this?). * Support inline images (link to bigger ones?) * hmm, these can be combined. See WikiWeblog * views (for visitors) * all items by time period (usually by editDate, but also offer by createDate) * just articles (by editDate or createDate) * search fulltext * RSS for weblogs.com Private writing: journal, psych, etc. * content * short journal entries * long journal entries * wiki pages of running themes (e.g. ADD) Storing content in XML would be nice, but WiKi is lite enough that it probably warrants a gimme on that criterion. I'm considering using ZopeCMF, with its CmfWiki, but my experiments so far have been a bit frustrating, and I think there will be significant blurring between "core" ZopeCMF content and CmfWiki content as time goes on, so I think I'll wait a bit. In the meantime... What needs to be added to ZWiki to make it better for diaries and blogging? * naming could be an issue. Does it make sense to try and assign a label to each entry? Should I just use a date/number? If I use a name, is it important to be able to rename? For now, thinking that using a date-driven name (e.g. B20011001A) for blog/diary entries can make sense for multiple reasons. * if deep wiki pages are in same space as little blog entries, RecentChanges listing won't be great for a blog reader. Would like (a) listing of new pages, then separate (b) listing of changed pages (moddate = today & createdate < today) * but createdate isn't even stored: can I live without it? * and how do I override moddate for first entry of historical stuff? * this is one reason for using special date-driven names for blog entries. * listings of pages need to show more than title. Entire document if fewer than 4 paragraphs, 1st paragraph if longer? * again, using custom name format for blog entries makes it easier to distinguish them. * maybe cache listings? * could end up with lots of pages, probably want some way to go back in time x pages (or days) at a time. * or is this really not necessary? * or is key to have structured query on range of dates? * want pretty intro page * would like to pass XML-RPC update flag to Dave, would like to have RSS view of my blog, etc. * once I'm comfortable with my set-up, will need to worry about MigratingContent. <hr> My grander vision is to integrate this into a broader structured data space (ObjectBrowser): PIM, ProjectManager, etc. Also see JornBarger (RobotWisdom) idea of archiving proxy server to log/cache everything you browse (PersonalWebArchive) <hr> Inspirations * Frontier, Pike, Manila * XanaDu * TheBrain (hides grandkids) * MindMapping (hides nothing) * TinderBox (EastGate) * InfoDepot and other OutLining packages * VisualTour feature of the main WikiWikiWeb * TouchGraph * other GraphDrawing tools * DiagramOfEffects (not so much a writing tool as a visual model) BillSeitzWritingToolHistory Some links from a recent thread on an email list: * list at MinciuSodas http://www.ms.lt/ms/projects/toolkinds/index.html * Wordit Prima http://www.wordit.com/prima/1.html * ThoughtStream for Palm http://thoughtstream.org/ * PeterMe bits on "info vis":http://peterme.com/archives/00000227.html "communication vis":http://peterme.com/archives/00000232.html , MartiHearst "interview":http://peterme.com/archives/00000233.html , ValdisKrebs "interview":http://peterme.com/archives/00000234.html * visualizations at http://www.bewitched.com/, including one for RhiZome http://www.rhizome.org/starrynight/ * Work from Digital Image Design http://didi.com/ * <hr> Have you already seen my http://www.ourpla.net/john/wikiweblogpim.html page? --JohnAbbe * reply: yep, definitely <hr> Have you seen my BlogFace? -KarlAnderson * hadn't seen it, see my questions on BlogFace page. -BillSeitz """ if __name__ == "__main__": unittest.main() --- NEW FILE: genericTextTest.py --- """Tests the GenericText filetype. Auxilary functions are tested in structuredTextTest.py. """ import unittest import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: sys.path.append("..") import genericText as gt import exceptions import outline class genericTextReaderTest(unittest.TestCase): def testGoodness(self): """structuredText: structuredTextReader works""" reader = gt.GenericTextReader flat = outline.BasicDocument() flat.addNewChild(childArgs=("Hello one.",)) flat.addNewChild(childArgs=("Hello two.",)) flat.addNewChild(childArgs=("Hello three.",)) indented = outline.BasicDocument() indented.addNewChild(childArgs=("Hello one.",)) indented[0].addNewChild(childArgs=("Hello two.",)) indented.addNewChild(childArgs=("Hello three.",)) small = outline.BasicDocument() small.addNewChild(childArgs=("Hello one.",)) small.addNewChild(childArgs=("Hello two.",)) s1 = reader(st1).outlineNodes() s2 = reader(st2).outlineNodes() s3 = reader(st3).outlineNodes() s4 = reader(st4).outlineNodes() s5 = reader(st5).outlineNodes() s6 = reader(st6).outlineNodes() self.assert_(s1.equivalentTo(flat)) self.assert_(s2.equivalentTo(indented)) self.assert_(s3.equivalentTo(indented)) self.assert_(s6.equivalentTo(indented)) self.assert_(s4.equivalentTo(small)) self.assert_(s2.equivalentTo(s3)) self.assert_(s5.equivalentTo(s1)) class genericTextSanityTest(unittest.TestCase): """For test data contained in the strings below this, does the string->reader->outlineNodes->writer->str->reader->outlineNodes produces two equal sets of outline nodes?""" def getTheNodes(self, s): r1 = gt.GenericTextReader(s) nodes1 = r1.outlineNodes() w1 = gt.GenericTextWriter(nodes1) s2 = w1.fileString() r2 = gt.GenericTextReader(s) nodes2 = r2.outlineNodes() return nodes1, nodes2 def testSanity(self): """structuredText: read == write->read (sanity)""" strList = [st1, st2, st3, st4, st5] for s in strList: n1, n2 = self.getTheNodes(s) self.assert_(n1.equivalentTo(n2)) self.assert_(n2.equivalentTo(n1)) st1 = """Hello one. Hello two. Hello three.""" st2 = """Hello one. Hello two. Hello three.""" st3 = """ Hello one. Hello two. Hello three. """ st4 = """ Hello one. Hello two.""" st5 = """\nHello one. Hello two. Hello three.""" st6 = """Hello one. \tHello two. Hello three. """ if __name__ == "__main__": unittest.main() --- NEW FILE: ilformatTest.py --- """Tests the Iron Lute native file format.""" import unittest import sys import os if __name__ != "__main__": f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: sys.path.append("..") sys.path.append("../..") import ilformat import outline import streams import xmlmapping ns = ilformat.ironluteNS def NS(s): return s.replace("%ns%", ns) nodeSimple1 = NS("<node xmlns='%ns%'>" "<data>testing</data></node>") class IronLuteFileFormatTest(unittest.TestCase): """Does the native Iron Lute format work?""" def testLinkElementMapping(self): """Iron Lute native files: Do LinkElements map?""" mapping = xmlmapping.XMLMapping(namespaces=[(ilformat.ironluteNS, '')]) mapper = xmlmapping.MapDef(ilformat.LinkElementMapper) mapper.update(klass = streams.LinkElement, tagName = '%s link' % ns) mapping.registerMapper(mapper) element = streams.LinkElement(1,2,3) xml = mapping.objToText(element) element2 = mapping.textToObj(xml) self.assert_(element == element2) def testHackyIntListMapping(self): "Iron Lute native files: Does the stupid, hacky int list mapper work?" mapping = xmlmapping.XMLMapping(namespaces=[(ilformat.ironluteNS, '')]) mapper = xmlmapping.MapDef(ilformat.IntListMapper) mapper.update(klass = list, tagName = "%s children" % ns) mapping.registerMapper(mapper) myIntList = [1, 44, 22, 124] xml = mapping.objToText(myIntList) self.assert_(xml == '<children xmlns="http://ironlute.' 'com/ironlute_nodes_v1"><child>1</child>' '<child>44</child><child>22</child><child>' '124</child></children>') listCopy = mapping.textToObj(xml) self.assert_(myIntList == listCopy) # Check the empty list myIntList = [] xml = mapping.objToText(myIntList) self.assert_(xml == '<children xmlns="http://ironlute.' 'com/ironlute_nodes_v1"></children>') listCopy = mapping.textToObj(xml) self.assert_(myIntList == listCopy) # Check bad types myIntList = ['not an int'] self.assertRaises(xmlmapping.NoMappingKnownError, mapping.objToText, myIntList) def testNodeElementMapping(self): """Iron Lute native files: Do Node Elements map?""" mapping = xmlmapping.XMLMapping(namespaces=[(ilformat.ironluteNS, '')]) mapper = xmlmapping.MapDef(ilformat.NodeElementMapper) mapper.update(klass = streams.NodeElement, tagName = '%s node' % ns) mapping.registerMapper(mapper) element = streams.NodeElement(1, 'ironlute.basic.node', 'schmoo hut', [3,4], 0) xml = mapping.objToText(element) newElement = mapping.textToObj(xml) self.assert_(newElement == element) def testBasicSaving(self): """Iron Lute native files: Does basic saving work?""" writer = ilformat.IronLuteNativeFileType.writerClass doc = outline.BasicNode() doc.addNewChild() doc[0].setData("Testing") text = writer(doc).fileString() reader = ilformat.IronLuteNativeFileType.readerClass doc2 = reader(text).outlineNodes() self.assert_(doc.equivalentTo(doc2)) if __name__ == "__main__": unittest.main() |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:11
|
Update of /cvsroot/ironlute/ironlute/simpleFile In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/simpleFile Added Files: __init__.py ftp.py http.py manila.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: http.py --- """http implements reading a URL with urllib. Or will.""" import sfile import urllib import urllib2 import urlparse # FIXME: Too much to mention :-) class HTTPFile(sfile.SimpleFileShell): """HTTPFile reads files over HTTP connections. Does not currently support writing.""" baseCapabilities = [sfile.CAP_READ, sfile.CAP_READCHUNK] group = "Web" name = "Simple HTTP file support" description = "A file hosting on a web server." paramList = [ ["URL", "The URL of the file", 'url'] ] def __init__(self, *params, **kwparams): sfile.SimpleFileShell.__init__(self, *params, **kwparams) self.file = None def humanName(self): urlParts = urlparse.urlparse(self.url) return self.url[self.url.rfind("/")+1:] + " on " + \ urlParts[1] def _openForReading(self): """Open the URL file object""" self.file = urllib.urlopen(self.url) def _read(self): """Download the text.""" page = self.file.read() self.file.close() self.file = None return page def _readChunk(self, len): return self.file.read(len) def _write(self): raise NotImplementedError def _close(self): if self.file is not None: self.file.close() self.file = None --- NEW FILE: __init__.py --- import os import sys import glob me = sys.modules[__name__] dir, filename = os.path.split(os.path.join(os.getcwd(), __file__)) for pythonFile in glob.glob(os.path.join(dir, "*py")): dir, filename = os.path.split(pythonFile) moduleName = filename[:-3] if moduleName != "__init__": exec "import %s" % moduleName del os, sys, glob, me, dir, filename, moduleName, pythonFile --- NEW FILE: manila.py --- """Manila implements several file types based on the Manila CMS from Userland Software. This file primarily exists because it was easy to write, and a good test of the SimpleFile system, showing off some more advanced examples. But these could be useful, hooked to a simpleText file type.""" import sfile import xmlrpclib class ManilaMessage(sfile.SimpleFileShell): """ManilaMessage implements a read-write interface to a Manila site message. For now, only plain text supported. As Iron Lute evolves, this should get more intelligent.""" baseCapabilities = [sfile.CAP_READ, sfile.CAP_WRITE, sfile.CAP_DELETE] group = "Manila" name = "Message" description = "A Manila message, of any kind." paramList = [ ["Hostname", "The hostname of your Manila site", 'serverName'], ["Username", "Your Manila username", 'username'], ["Password", "Your password", 'password'], ["Message Number", "The number of the desired message", 'msgNum'] ] def __init__(self, *params, **kwparams): SimpleFileShell.__init__(self, *params, **kwparams) self.server = xmlrpclib.Server("http://%s/RPC2" % self.serverName) self.siteName = None def resolveSiteName(self): if self.siteName is None: try: self.siteName = self.server.manila.getSiteName( "http://%s/" % self.serverName) except: self.baseCapabilities = [] raise CantReadError("Could not resolve the name of " "the Manila site, so no reading " "was done.") def _openForReading(self): """This file type does not need to be 'opened', but this is a good time to translate the server name to a Manila site name, if it has not already been done..""" self.resolveSiteName() def _openForWriting(self): """This file type does not need to be 'opened', but this is a good time to translate the server name to a Manila site name, if it has not already been done..""" self.resolveSiteName() def _read(self): try: message = self.server.manila.message.get(self.username, self.password, self.siteName, self.msgNum) return message['body'] except xmlrpclib.Fault, f: raise CantReadError(f.faultString) else: raise def _write(self, s): try: message = self.server.manila.message.set(self.username, self.password, self.siteName, self.msgNum, '', s, 'text/plain', {}, {}) except xmlrpclib.Fault, f: raise CantWriteError(f.faultString) except: raise def _close(self): """This file type does not need to be closed.""" --- NEW FILE: ftp.py --- import sfile import ftplib import cStringIO # This is a simple FTP simplefile. It actually tries to cache the # connection and re-use it; FIXME we'll need to work this into # simpleFiles DEFAULT_PORT = 21 class FTPFile(sfile.SimpleFileShell): """FTPFile does full reading and writing, but no streaming.""" baseCapabilities = [sfile.CAP_READ, sfile.CAP_WRITE] group = "Internet" name = "FTP file support" description = "A file on an FTP server." paramList = [ ['Server', "The FTP server the file is on", 'server'], ['Username', "The username to log into the server", 'username', 'anonymous'], ['Password', "The password. (For 'anonymous', etiquette is" " to send your email.)", 'password'], ['Filename', "The filename, including directory.", "filename"], ['Port', "The port (ignore unless you know otherwise)", 'port', DEFAULT_PORT], ] def __init__(self, *params, **kwparams): sfile.SimpleFileShell.__init__(self, *params, **kwparams) self.connection = None def humanName(self): filename = self.filename.split("/")[-1] servername = self.server if self.port != DEFAULT_PORT: servername += ":" + str(self.port) return filename + " on FTP at " + servername def connect(self): # FIXME: Check status if self.connection: return self.connection = ftplib.FTP() self.connection.connect(self.server, int(self.port)) self.connection.login(self.username, self.password) def disconnect(self): if self.connection: self.connection.quit() self.connection = None def _read(self): pieces = [] def getPiece(p): pieces.append(p) self.connection.retrbinary('RETR %s' % self.filename, getPiece) return "".join(pieces) def _write(self, s): f = cStringIO.StringIO(s) self.connection.storbinary('STOR %s' % self.filename, f) def _openForWriting(self): self.connect() def _openForReading(self): self.connect() def _close(self): self.disconnect() |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:10
|
Update of /cvsroot/ironlute/ironlute/skel In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/skel Added Files: startup.opml Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: startup.opml --- <opml> <head> </head> <body> <outline text="Wow, is this out of date. MMMMMMMMMMMMM MMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMM MMMMMMMMMMMMMMMMMMMMMM M" /> <outline text="Welcome back to Iron Lute!" /> <outline text="Milestones/New Features"> <outline text="This document was actually created inside of Iron Lute from scratch, rather then created by editing OPML directly in Emacs." /> <outline text="A few new commands in the Outline menu, notably Move Node Up and Move Node Down. In addition, Dedent and Indent, normally used with Tab and Shift-Tab (which still work) can be done with CTRL-Left arrow and CTRL-Right arrow, allowing a single node to be rapidly moved around the outline with just the keyboard." /> <outline text="ZWiki node support, discussed under its own top-level heading." /> <outline text='Split, invokable as a menu option or with the Enter key, and join, invokable with a menu option or by hitting "Backspace" on the first char in the line.' /> <outline text="Display now scrolls around on lines, instead of nodes, giving a much smoother appearence and feel." /> <outline text="It's not great, it's not even good, but it's starting to look like an actual outliner." /> <outline text="New bugs, I'm sure." /> </outline> <outline text="Editing ZWiki Nodes"> <outline text="To edit ZWiki nodes, use File->Open ZWiki Node... A dialog box will appear asking for the URL, user name, and password. As will be explained when the help system gets created, the URL should be the actual browser-viewable URL of the node you wanto edit." /> <outline text="Username and password are optional, and will only come into play if the edits you make require authentication. I have confirmed that this works when authentication is not required. I have not been able to test the saving of a ZWiki node when authentication is required. I also have not been able to create new ZWiki nodes programmatically; I tried but I don't know what I was missing, it should have worked." /> <outline text="Getting ZWiki nodes to save with authentication"> <outline text="I see from your site that you have used Python. If it turns out that ZWiki node saving with authentication doesn't work, the function to examine is _write(self, s) in class ZWikiNode(SimpleFileShell) in ironlute/simpleFile/zwiki.py . You can play with getting that right if you like. If you do, please send me the fix. It *should* be easy, it may even already work, I just can't test it." /> <outline text="Everything else should work fine and not require any fiddling, at least to a first approximation. (Sorry about contaiminating my entry on your site, BTW... I had assumed I would be able to remove it by myself.)" /> </outline> <outline text="Structured Text"> <outline text="As you will see, I did not try to do much more then parse the levels of the structured text, which should be adequate for now. Later I want to at least extract "what kind of line is this?", i.e., list element, numbered list element, etc. Since right now, there's nowhere for that data to go, there's not much point in extracting it." /> <outline text="The only oddity will be if you use :: to introduce pre-formatted text; the act of reading in the :: text and re-saving may mangle the spacing. I know how to fix it, but again, the infrastructure to handle that correctly does not currently exist." /> </outline> </outline> <outline text="Immediate Next Steps"> <outline text="Introduce node types and document types." /> <outline text="Get the help system going." /> <outline text="Continue to bang on the UI, and continue adding commands until this thing actually feels like a real program, rather then a hack." /> </outline> <outline text="XML-RPC directory (http://www.xmlrpc.com/discuss/reader$1568.opml)" type="link" url="http://www.xmlrpc.com/discuss/reader$1568.opml" /> </body> </opml> |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:10
|
Update of /cvsroot/ironlute/ironlute/nodetypes/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/nodetypes/tests Added Files: standardnodeTest.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: standardnodeTest.py --- import sys import os if __name__ != "__main__": f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: if os.pardir not in sys.path: sys.path.append(os.pardir) sys.path.append(os.pardir + os.sep + os.pardir) import unittest import outline import standardnodes class standardNodeTest(unittest.TestCase): """Validate the basic functionality of the Standard Node framework.""" def testStandardNode(self): """Standard Nodes: testing basic properties""" defaultVal = 44 testVal = 41 class StandardNodeTest(standardnodes.StandardNode): dataDescription = (('test', 'a test property', defaultVal),) node = StandardNodeTest() self.assert_(node.test == defaultVal) node.test = testVal self.assert_(node.test == testVal) del node.test self.assert_(node.test == defaultVal) if __name__ == "__main__": unittest.main() |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:09
|
Update of /cvsroot/ironlute/ironlute/nodetypes In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/nodetypes Added Files: __init__.py commandnodes.py standardnodes.py transcluder.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: __init__.py --- from commandnodes import * from transcluder import * --- NEW FILE: transcluder.py --- """TransclusionDocuments are nodes specifically design to easily enable transclusion-based nodes. They transparently aquire the characteristics of the nodes they are transcluding for for the normal outline UI, but allow things that care, like the GUI and the file serializers, to treat these nodes specially. TransclusionDocuments use the fileData data source. Transcluder nodes are documents, because they will expand to become documents.""" import outline import ilIndex class TransclusionDocument(outline.BasicDocument): """Represents a transcluded document.""" IndexerKey = "internal" def __init__(self, parent = None, readOnly = 0, simpleFile = None, fileType = None): outline.BasicDocument.__init__(self, parent = parent, readOnly = readOnly) datesources = ilIndex.datasources self.data = datasources.FileData(self, simpleFile = simpleFile, fileType = fileType) --- NEW FILE: standardnodes.py --- """StandardNodes are node types that implement the various standard node types, like Ordered List Nodes and such.""" import outline class AttRetr(object): """A descriptor that takes a name and returns something out of the host class's .atts dict.""" def __init__(self, attName, default): self.attName = attName self.default = default def __get__(self, obj, type = None): if self.attName in obj.atts: return obj.atts[self.attName] return self.default def __set__(self, obj, value): obj.atts[self.attName] = value def __delete__(self, obj): # note this will actually 'reveal' the default del obj.atts[self.attName] class DescribedData(outline.NodeIndexer): """This metaclass takes a DataDescription, specified on the .dataDescription class member. This attribute is *required* for classes that have this metaclass. Data specifications are specified as a tuple of 3-ples, with the following things in them: * name * description of what the property represents in English (*required*). This is used by humans only, but I'm requiring it because it is going to be important to be very clear about what every property is. * The default value; this should be a sort of "neutral" value that can be safely ignored. (Python None is acceptable, missing attribute errors are not.) Note that the property generation is incidental... it is the list of all properties that is useful.""" # FIXME: the __doc__ ought to have some autogenerated docs # from this appended to it. def __init__(cls, name, bases, dict): super(DescribedData, cls).__init__(name, bases, dict) try: if len(cls.dataDescription): for name, desc, default in cls.dataDescription: setattr(cls, name, AttRetr(name, default)) except ValueError, e: if e.args[0] == "too many values to unpack": e.args = ("too many values to unpack (did you forget " "the comma after a one-tuple? i.e., for " "one property, (('a','b',0),) with the comma" " inside the outer parens?") raise class StandardNode(outline.BasicNode): """A StandardNode is some cross-format common type of node that has some standardized data associated with it, which each format may use or not use. A StandardNode has a DataSpecification associated with it as a class member dataSpecification, which describes the optional data associated with it. This helps with conversions between types and such. To fully understand these nodes, be sure to consult the dataDescription.""" __metaclass__ = DescribedData dataDescription = () def __init__(self): super(StandardNode, self).__init__() # storage for the properties self.atts = {} class HeaderNode(StandardNode): """A Header represents a header.""" dataDescription = ( ('level', 'The "level" of the heading. 0 is the top-level heading, and ' 'higher numbers represent subsequently lower headings. To ' 'convert from HTML, for instance, take the H? and subtract ' 'one from the number.', # FIXME: This node will need some support for normalizing # the header numbers 0 ), ) # A superclass for list elements; they aren't *quite* the same class ListElement(StandardNode): pass class OrderedListElementNode(ListElement): """Represents an ordered list element, like <ol> in HTML.""" dataDescription = ( ) class UnorderedListElementNode(ListElement): """Represents an unordered list element, like <ul> in HTML.""" dataDescription = ( ) class QuoteNode(StandardNode): """Represents the various kinds of quotes, like <blockquote>.""" dataDescription = ( ) class PreformattedNode(StandardNode): """Represents preformatted data, like <pre> in HTML. Preformatted data should be displayed in a manner dependent on the output, and in general can not have tags on it.""" dataDescription = () --- NEW FILE: commandnodes.py --- """CommandNodes are node types relating to the commands, including menus. CommandMenuDocuments implement things that create new command menus, and CommandNodes implement a method that allows the user to set the command shortcuts for the given command.""" from __future__ import generators import outline import registry import constants import ilIndex # FIXME: submenus (low priority), one meta-menu document. class MenuEntryNode(outline.BasicNode): """MenuEntryNode is the parent class for menu entries. It defines a method that children must have.""" IndexerKey = "internal" def isSeparator(self): return 0 class CommandMenuDocument(outline.BasicDocument): """CommandMenuDocuments wrap a menu, which consists of a series of CommandMenuNodes, Text Nodes (for submenus), and Seperator Nodes. This implements the menus in Iron Lute.""" IndexerKey = "internal" allowedChildren = ["internal.MenuEntryNode"] canSave = 0 def __init__(self, dispatcher = None): self.dispatcher = dispatcher outline.BasicDocument.__init__(self) def addCommand(self, cmd, pos = None): self.addChild(CommandNode(cmd), pos) def addSeparator(self, pos = None): self.addChild(SeparatorNode(), pos) def generateMenu(self, menu, outlineFrame, context = None): """generateMenu takes the stored commands, and generates a sequence of method class to a GUI menu builder that will build this menu. menu is a reference to some GUI menu generating object that will take those methods and build the actual menu. This can take a context, in which case the method will pass that context to all dispatcher calls.""" import dispatcher for cmdLink in self.getRootHandle(): cmd = cmdLink.link.dest if cmd.isSeparator(): menu.ilAddSeparator() continue c = cmd.getCommandClass() accelerator = None accelStr = "" if self.dispatcher is not None: accelerator = self.dispatcher.queryKeypress(c.classId, context) if accelerator is not None: accelStr = dispatcher.keyParamsToText(*accelerator) state = c.canDo(outlineFrame) menu.ilAddCommand(c, accelerator, accelStr, state) def getMenuEntries(self, rightclick = 0): return ["ironlute.menus.AddSeparator", 'ironlute.menus.AddCommand'] class SeparatorNode(MenuEntryNode): """Represents a separator in the menus.""" allowedChildren = [] # leaf type def isSeparator(self): return 1 def getData(self, key = None): if key is None: return "----------" else: return MenuEntryNode.getData(self, key) class CommandNode(MenuEntryNode): """Represents a command entry in the menu.""" allowedChildren = [] # leaf type def __init__(self, cmd = None, parent = None): """Create a CommandNode with the given command and parent. cmd must be None or one of the three acceptable command specifications: a string naming the command, an instance of the CommandArgs class, or the command class itself.""" # We do not allow direct manipulation of the text; you # must go through the manipulation commands MenuEntryNode.__init__(self, parent = parent, readOnly = 1) self.cmd = cmd self.getCommandClass().subscribe(self) def getDispatcher(self): doc = self.getDocument() return doc.dispatcher def getCommandClass(self): import command if type(self.cmd) == type(""): return ilIndex.cmds.getByClassId(self.cmd) elif isinstance(self.cmd, command.CommandArgs): return self.cmd.getCommand() return self.cmd def getMenuEntries(self, rightclick = 0): return ['ironlute.menus.AddBinding', 'ironlute.menus.ClearBindings'] # Overriding node methods: getData should return the name of the # command for the GUI. def getData(self, key = None): if key is None: dispatcher = self.getDispatcher() cmd = self.getCommandClass() keypresses = dispatcher.queryObj(cmd.classId, asStr = 1) if len(keypresses) == 0: return cmd.name else: keystr = ' (%s)' % ", ".join(keypresses) return cmd.name + keystr else: return MenuEntryNode.getData(self, key) def notifyCommandEvent(self, event): self.notifySubscribers(outline.NodeEvent("data_change", self)) class DynamicCommandMenu(object): OutlineIndex = 1 """DynamicCommandMenus are CommandMenus that take a function they use to get a CommandMenu object to delegate the importent functions to.""" IndexerKey = "internal" def __init__(self, getCommandMenuFunc, dispatcher = None): self.getCommandMenuFunc = getCommandMenuFunc self.dispatcher = dispatcher def __iter__(self): menu = self.getCommandMenuFunc() for cmd in iter(menu): yield cmd def generateMenu(self, menu, outlineFrame, context = None): commandMenu = self.getCommandMenuFunc() if commandMenu is None: menu.ilDisable() return commandMenu.dispatcher = self.dispatcher commandMenu.generateMenu(menu, outlineFrame, context) |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:09
|
Update of /cvsroot/ironlute/ironlute/filetypes In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/filetypes Added Files: genericText.py ilformat.py structuredText.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: structuredText.py --- """StructuredText reads the semi-standardized StructuredText, as described at http://webseitz.fluxent.com/wiki/StructuredTextRules. FIXME: Find the real link. Right now, it just reads the structured text directly and only does the indentation-based structuring, it doesn't try to create ordered text or use special node types. It will later. (First things first.)""" import sys if ".." not in sys.path: sys.path.append("..") import ilIndex import outline import re import constants import filetypes whitespace = " \t" def indentLevel(string): """Using the def given on the ZWiki help pages, the indent level is the minimum number of spaces a paragraph leads with. A tab == one space. This function requires \n delimited paragraphs, and assumes the paragraphs have already been split so it doesn't have to check for \n\n.""" if not isinstance(string, str): raise TypeError("Can't determine indent level of a" " non-string.") minIndent = None lines = string.split("\n") for line in lines: if line: count = 0 linelen = len(line) while count < linelen and (line[count] in whitespace): count += 1 if minIndent is None: minIndent = count else: minIndent = min(minIndent, count) return minIndent def extractText(string): """Takes a 'paragraph', which may contain \\n's and leading whitespace, and wipes out the \\n's and leading whitespace.""" if not isinstance(string, str): raise TypeError("Can't extract text from a" " non-string.") lines = [x.lstrip() for x in string.split("\n")] return " ".join(lines).rstrip() def normalizeLineFeeds(s): """Takes a string with unknown line feed types, and normalizes it to only use \\n.""" if not isinstance(s, str): raise TypeError("Can't normalize lines feeds in a" " non-string.") s = s.replace("\r\n", "\n") s = s.replace("\r", "\n") return s class StructuredTextReader: """Reads in StructuredText and spits out outline nodes. Basic structure: * Keep a list of previous indentation levels. When we exceed the previous highest, we indent. When we go below the highest, we dedent far enough back to get to the appropriate level. When we are at the same level, we stay on the same level. * March through the paragraphs with those rules, striping out \\n's and initial whitespace on new lines, if necessary. """ def __init__(self, s): try: self.s = s.read() except: self.s = str(s) def outlineNodes(self, subscriber = None): # Step one: Normalize line feeds self.s = normalizeLineFeeds(self.s) paragraphs = re.split(r"\n[ \t]*\n+", self.s) paragraphs = [x for x in paragraphs if x.strip() != ""] if not paragraphs[0]: paragraphs = paragraphs[1:] levels = [indentLevel(paragraphs[0])] root = ilIndex.nodes.BasicDocument() if subscriber is not None: root.subscribe(subscriber) self.stack = [root] for p in paragraphs: while p[0] == "\n" or p[0] == "\r": p = p[1:] #p = p.replace("\n", " ") if self.suppressNode(p): continue thisLevel = indentLevel(p) # The three cases: Same, indent, dedent. # 'same' is handled in passing if thisLevel > levels[-1]: levels.append(thisLevel) self.stack.append(self.stack[-1][-1]) if thisLevel < levels[-1]: while levels and thisLevel < levels[-1]: levels.pop(-1) self.stack.pop(-1) if not levels: # looks like we ran out of levels levels.append(thisLevel) self.stack.append(root) # Kludge: If the 'thisLevel' is > # the remaining level[-1], reset level[-1] # to this new value... this is really # invalid StructuredText probably. levels[-1] = thisLevel # This allows various parsers based off of this newNode = self.getNodeFromText(extractText(p)) if newNode is not False: self.stack[-1].addChild(newNode) self.postprocessNode(newNode) return root def suppressNode(self, text): # if True is returned, the node is skipped in the processing return False def getNodeFromText(self, text): node = outline.BasicNode(); node.setData(text) return node def postprocessNode(self, node): pass class JerfTextReader(StructuredTextReader): def __init__(self, *args, **kwargs): StructuredTextReader.__init__(self, *args, **kwargs) self.nodetypes = {"*": ilIndex.nodes.UnorderedListElementNode, "#": ilIndex.nodes.OrderedListElementNode, ")": ilIndex.nodes.QuoteNode, "%": ilIndex.nodes.PreformattedNode} def getNodeFromText(self, text): # Rules: # * -> unordered list # # -> ordered list # ( -> quote # & -> subheading, equal in strength to number of & # (no & + children -> heading 0) # ! -> metadata - no node output, first word is the key, # rest is the value # % -> Preformatted node root = self.stack[0] for symbol, nodetype in self.nodetypes.iteritems(): if text.startswith(symbol): node = nodetype() if symbol == "%": text = text[2:] else: text = text[1:].strip() node.setData(text) return node if text.startswith("&"): node = ilIndex.nodes.HeaderNode() level = text.count("&", 0, 4) text = text[level:].strip() node.setData(text) node.level = level return node node = ilIndex.nodes.BasicNode() node.setData(text) return node def suppressNode(self, text): if text.startswith("!"): text = text[1:].strip() name, value = text.split(None, 1) self.stack[0].setData(value, name) return True return False def postprocessNode(self, node): if isinstance(node, ilIndex.nodes.HeaderNode.getObject()): return # We know this is a tree i = node.incoming parent = list(node.incoming.data[node.document])[0].source if parent is self.stack[0]: return if parent.__class__.__name__ == "HeaderNode": return index = parent.getChildIndex(node) if index == 0 and \ isinstance(parent, ilIndex.nodes.BasicNode.getObject()) and \ not isinstance(node, ilIndex.nodes.QuoteNode.getObject()): # Header! newNode = ilIndex.nodes.HeaderNode() parent.replaceWith(newNode, preserveData = True) self.stack[self.stack.index(parent)] = newNode class StructuredTextWriter: """Converts a node into a structuredTextDocument.""" joiner = "\n\n" def __init__(self, outlineRoot, isFragment = 0): self.outlineRoot = outlineRoot self.chunks = [] self.isFragment = 0 def append(self, s, depth): self.chunks.append(" " * depth + s) def fileString(self): self.chunks = [] depth = -1 rootHandle = self.outlineRoot.getRootHandle() for token in rootHandle.depthFirst(suppressRoot = 1, yieldMarkers = 1): if token == constants.CHILDREN: depth += 1 elif token == constants.END_CHILDREN: depth -= 1 else: self.append(token.getNode().getData(), depth) if self.chunks: self.chunks[0] += "\n" # put a linefeed on the end return self.joiner.join(self.chunks) notes = """ Potential quirks: * If the structured text starts out indented, that will be assumed to be the "left limit" of the text. If it drops back behind that, it will be treated that as the new limit, and will not try to 'fix' the old text. """ class StructuredTextFileType(filetypes.FileType): name = "StructuredText" description = "Structured Text simple text formatting." moreInfo = "http://webseitz.fluxent.com/wiki/StructuredTextRules" readerClass = StructuredTextReader writerClass = StructuredTextWriter notes = notes status = constants.ALPHA | constants.FULL_READ | \ constants.PARTIAL_WRITE class JerfWebsiteFileType(StructuredTextFileType): name = "Jerf's Website File Type" description = "Simple formatting for a website hack. Probably " \ "temporary, but we all know what that means. Not " \ "meant for output." readerClass = JerfTextReader isFileType = True status = constants.ALPHA | constants.FULL_READ --- NEW FILE: ilformat.py --- """This uses the xmlmapping library to create a mapping from Iron Lute's native data format into XML, and back to the native format. Given that the XMLMapping library is a disaster zone, I wouldn't expect this to be much more comprehensible.""" # This assumes that the checking for living in an Iron Lute # environment has been done (probably by __init__.py) and will only be # imported if the environment is correct. This allows us to avoid # indenting the entire module underneath an if, in case this gets used # as a library where Iron Lute doesn't exist. import sys import os if __name__ != "__main__": # add parent dir to path f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: if ".." not in sys.path: sys.path.append("..") # This is based on the xmlmapping library # Oh boy, here we go. import filetypes import xmlmapping as xmlm import outline import streams from streams import xmlobjects as xmlo from streams.outlinestream import LinkElement, NodeElement from xmlmappingconstants import * ironluteNS = "http://ironlute.com/ironlute_nodes_v1" mm = xmlm.MultipartMapper # Let's start with a LinkElementMapper # FIXME: This does not handle "other" data def breakupLinkElement(element): yield ('id', element.id) yield ('source', element.source) yield ('dest', element.dest) # FIXME: This needs better abstraction in xmlmapping class LinkElementMapper(mm): def __init__(self, *args, **kwargs): # Prepare the part mappers id = xmlm.MapDef(xmlm.SimpleDelimitedTextMapper) id.update(tagName = "%s id" % ironluteNS, klass = tuple) source = xmlm.MapDef(xmlm.SimpleDelimitedTextMapper) source.update(tagName = "%s source" % ironluteNS, klass = tuple) dest = xmlm.MapDef(xmlm.SimpleDelimitedTextMapper) dest.update(tagName = "%s dest" % ironluteNS, klass = tuple) kwargs['partMappers'] = [[id, lambda o: ('id', unicode(o.id))], [source, lambda o: ('source', unicode(o.source))], [dest, lambda o: ('dest', unicode(o.dest))]] mm.__init__(self, *args, **kwargs) self.atts = {} def presentObject(self, obj): self.atts[obj[0]] = obj[1] def endMapper(self): mm.endMapper(self) for name in ('id', 'source', 'dest'): if name not in self.atts: err = xmlm.CantHandleStreamError raise err("LinkElement couldn't be constructed " "because <%s> was missing." % name) element = streams.LinkElement(int(self.atts['id']), int(self.atts['source']), int(self.atts['dest'])) self.xmlMapper.propogateFinishedObject(element) class NodeElementMapper(mm): def __init__(self, *args, **kwargs): id = xmlm.MapDef(xmlm.SimpleDelimitedTextMapper) id.update(tagName = "%s id" % ironluteNS, klass = tuple) nodetype = xmlm.MapDef(xmlm.SimpleDelimitedTextMapper) nodetype.update(tagName = "%s nodetype" % ironluteNS, klass = tuple) data = xmlm.MapDef(xmlm.SimpleDelimitedTextMapper) data.update(tagName = "%s data" % ironluteNS, klass = tuple) isroot = xmlm.MapDef(xmlm.SimpleDelimitedTextMapper) isroot.update(tagName = "%s root" % ironluteNS, klass = tuple) children = xmlm.MapDef(IntListMapper) children.update(tagName = "%s children" % ironluteNS, klass=list) kwargs['partMappers'] = [ [id, lambda o: ('id', unicode(o.id))], [nodetype, lambda o: ('nodetype', unicode(o.nodeType))], [data, lambda o: ('data', unicode(o.data))], [children, lambda o: o.children], [isroot, lambda o: ('root', unicode(o.isRoot))] ] mm.__init__(self, *args, **kwargs) self.atts = {} def presentObject(self, obj): # Hack hack hack; checking based on type from the submapper # bleh if isinstance(obj, tuple): self.atts[obj[0]] = obj[1] if isinstance(obj, list): self.atts['children'] = obj def endMapper(self): mm.endMapper(self) for name in ('id', 'nodetype', 'data', 'children'): if name not in self.atts: err = xmlm.CantHandleStreamError raise err("NodeElement couldn't be constructed " "because <%s> was missing." % name) if 'isroot' not in self.atts: self.atts['isroot'] = 0 element = streams.NodeElement(int(self.atts['id']), unicode(self.atts['nodetype']), unicode(self.atts['data']), self.atts['children'], int(self.atts['isroot'])) self.xmlMapper.propogateFinishedObject(element) class IntListMapper(xmlm.Mapper): """A mapper that maps a list of ints to and from XML. FIXME: This is a massive cheat because xmlmapping in its current form in too weak; this shouldn't be necessary but I'm bootstrapping.""" NOT_IN_CHILD = 0 IN_CHILD = 1 def __init__(self, xmlMapper, klass = None, tagName = None, direction = None): submappers = [] intMapperDef = xmlm.MapDef(xmlm.SimpleDelimitedTextMapper) intMapperDef.update(objToText = lambda o: ('%s child' % ironluteNS, str(o))) intMapperDef.update(textToObj = lambda n, v: int(v)) intMapperDef.update(tagName = '%s child' % ironluteNS, klass = int) submappers.append(intMapperDef) xmlm.Mapper.__init__(self, xmlMapper, submappers = submappers, direction = direction) self.state = self.NOT_IN_CHILD self.intList = [] if direction == XML2OBJ: self.xmlMapper.pushObj(self) def objToXML(self, obj, output): """Outputs the child list to the stream.""" output(xmlo.StartElement("%s children" % ironluteNS, {})) for i in obj: self.xmlMapper.put(i) output(xmlo.EndElement("%s children" % ironluteNS)) def XMLToObj(self, stream): # 'presentObject' does all the work, this just avoids # the "Unimplemented" error if I don't have this here. pass def endMapper(self): self.xmlMapper.popObj() self.xmlMapper.propogateFinishedObject(self.intList) def presentObject(self, obj): self.intList.append(int(obj)) return 1 m = xmlm.XMLMapping(namespaces=[(ironluteNS, '')]) linkElementMapper = xmlm.MapperDefinition(LinkElementMapper) linkElementMapper.update(klass = LinkElement, tagName = "%s link" % ironluteNS) m.registerMapper(linkElementMapper) nodeElementMapper = xmlm.MapperDefinition(NodeElementMapper) nodeElementMapper.update(klass = NodeElement, tagName = "%s node" % ironluteNS) m.registerMapper(nodeElementMapper) # A dummy, to ignore the <ironlute> element if I see it ironluteMapper = xmlm.MapperDefinition(xmlm.DummyMapper) ironluteMapper.update(tagName = "%s ironlute" % ironluteNS) m.registerMapper(ironluteMapper) # This component slaps the <ironlute>, </ironlute>, and # namespace declarations around all the link and node # elements. class DocumentWrapComponent(streams.Component): def __init__(self): streams.Component.__init__(self) self.started = 0 def put(self, obj): if not self.started: self.output(xmlo.StartNamespaceDecl('', ironluteNS)) self.output(xmlo.StartElement("%s ironlute" % ironluteNS, {'version': "0.1a"})) self.started = 1 self.output(obj) return if obj is streams.EndOfStream: self.output(xmlo.EndElement("%s ironlute" % ironluteNS)) self.output(obj) return # Otherwise, be a passthrough element self.output(obj) class IronLuteWriter(filetypes.BasicWriter): """Converts a node into an Iron Lute document.""" # FIXME: By default, we want to save w/ GZip. def __init__(self, outlineRoot): self.outlineRoot = outlineRoot def fileString(self): # Create a stream to collect the XML s = streams.Stream() # Append the outline -> pieces component s.appendComponent(streams.NodeToElements()) # Pieces -> XML Objects s.appendComponent(m.getObj2XMLMapping()) # Document wrapper; see above s.appendComponent(DocumentWrapComponent()) # XML Objects -> XML Text s.appendComponent(streams.XMLToStringComponent()) # Throw the node in one end... s.put(self.outlineRoot) s.endStream() # ... get the text out the other. return "".join(s) class IronLuteReader(filetypes.BasicReader): """Converts a stream or a string into a node.""" def __init__(self, source): self.source = source def outlineNodes(self, subscriber = None, root = None): """Parses the source. Returns an exception on failure.""" s = streams.Stream() # Tack on the conversion components on the end of the stream: # XML -> XML Objects s.appendComponent(streams.ExpatXMLComponent()) # XML objects -> outline stream pieces s.appendComponent(m.getXML2ObjMapping()) # outline stream -> node s.appendComponent(streams.ElementsToNode()) s.put(self.source) s.endStream() return s.outputs[0].get() class IronLuteNativeFileType(filetypes.FileType): name = "Iron Lute Native" description = "The Iron Lute Native file type, built to match " \ "the capabilities of Iron Lute precisely. Saving "\ "in this format is gauranteed to save your " \ "outline without loss, barring bugs." writerClass = IronLuteWriter readerClass = IronLuteReader --- NEW FILE: genericText.py --- """GenericText tries to read and write generic text. For input, it differs from structuredText in that all lines are considered seperate indentations even if there is no blank space between them. For the writer, it uses tabs and no blank lines between the various lines instead.""" import structuredText import re import outline import streams class GenericTextReader: def __init__(self, stream): # Wrap a NormalizeLineFeeds stream around the stream self.s = stream def outlineNodes(self, subscriber = None): # FIXME: move normalizeLineFeeds into support self.s = structuredText.normalizeLineFeeds(self.s) # FIXME: Factor out the differences in structuredText paragraphs = re.split("\n", self.s) paragraphs = [x for x in paragraphs if x.strip() != ""] if not paragraphs[0]: paragraphs = paragraphs[1:] # from structuredText, why? levels = [structuredText.indentLevel(paragraphs[0])] root = outline.BasicDocument() if subscriber is not None: root.subscribe(subscriber) self.stack = [root] for p in paragraphs: while p[0] == "\n" or p[0] == "\r": p = p[1:] thisLevel = structuredText.indentLevel(p) if thisLevel > levels[-1]: levels.append(thisLevel) self.stack.append(self.stack[-1][-1]) if thisLevel < levels[-1]: while levels and thisLevel < levels[-1]: levels.pop(-1) self.stack.pop(-1) if not levels: levels.append(thisLevel) self.stack.append(root) levels[-1] = thisLevel newNode = outline.BasicNode() self.stack[-1].addChild(newNode) newNode.setData(structuredText.extractText(p)) return root class GenericTextWriter(structuredText.StructuredTextWriter): joiner = "\n" def append(self, s, depth): self.chunks.append("\t" * depth + s) class GenericTextFileType(structuredText.StructuredTextFileType): name = "GenericText" description = "An attempt to do generic text input and output." readerClass = structuredText.StructuredTextReader writerClass = structuredText.StructuredTextWriter |
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:08
|
Update of /cvsroot/ironlute/ironlute/datasources/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/datasources/tests Added Files: fileDataTest.py noChildrenTest.py testData.opml Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: noChildrenTest.py --- """Tests the NoChildren data source.""" import sys import os if __name__ != "__main__": f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: if os.pardir not in sys.path: sys.path.append(os.pardir) sys.path.append(os.pardir + os.sep + os.pardir) import unittest import noChildren import outline class NoChildrenTest(unittest.TestCase): def testFunctionality(self): """NoChildren Data Source: Works?""" document = outline.BasicDocument(dataSource = noChildren.NoChildrenData) self.assert_(isinstance(document.data, noChildren.NoChildrenData)) self.assert_(document.hasChildren() == 0) self.assertRaises(NotImplementedError, document.addChild, document) self.assertRaises(NotImplementedError, document.getChildIndex, document) self.assertRaises(NotImplementedError, document.getChildren) self.assert_(document.getChildCount() == 0) document.setData('a') self.assert_(document.getData() == 'a') if __name__ == "__main__": unittest.main() --- NEW FILE: testData.opml --- <opml version="1.0"> <head> </head> <body> <outline text="this is a test data file for the transcluder" /> <outline text="this file has three children"> <outline text="and the second child has a child" /> </outline> <outline text="this is the third child" /> </body> </opml> --- NEW FILE: fileDataTest.py --- """Tests the file data source.""" import sys import os if __name__ != "__main__": f = __file__ f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: if os.pardir not in sys.path: sys.path.append(os.pardir) sys.path.append(os.pardir + os.sep + os.pardir) import unittest import ilIndex import fileData import nodeData class FileDataTest(unittest.TestCase): def testFunctionality(self): """File Data Source: Works?""" document = ilIndex.nodes.BasicDocument(dataSource=fileData.FileData) dir = os.path.dirname(__file__) dir = os.path.join(os.getcwd(), dir) dir = os.path.normpath(dir) opml = os.path.join(dir, "testData.opml") sf = ilIndex.simpleFile.File(opml) self.assert_(isinstance(document.data, fileData.FileData)) self.assert_(document.hasChildren()) self.assertRaises(ValueError, document.addChild) self.assertRaises(ValueError, document.getChildIndex, document) self.assertRaises(ValueError, document.getChildCount) self.assertRaises(ValueError, document.getChildren) document.setData('a') self.assert_(document.getData() == 'a') document.data.fileType = ilIndex.filetypes.OPMLFileType.getObject() self.assert_(document.hasChildren() == 1) self.assertRaises(ValueError, document.addChild) self.assertRaises(ValueError, document.getChildIndex, document) self.assertRaises(ValueError, document.getChildCount) self.assertRaises(ValueError, document.getChildren) document.setData('b') self.assert_(document.getData() == 'b') document.data.fileType = None document.data.simpleFile = sf self.assert_(document.hasChildren() == 1) self.assertRaises(ValueError, document.addChild) self.assertRaises(ValueError, document.getChildIndex, document) self.assertRaises(ValueError, document.getChildCount) self.assertRaises(ValueError, document.getChildren) document.setData('c') self.assert_(document.getData() == 'c') document.data.fileType = ilIndex.filetypes.OPMLFileType.getObject() self.assert_(document.getChildCount() == 3) self.assert_(document[1].getData() == "this file has three children") self.assert_(isinstance(document.data, nodeData.NodeData)) self.assert_(document.getData() == 'c') if __name__ == "__main__": unittest.main() |