tag:blogger.com,1999:blog-83378714044394789792024-03-08T22:47:15.100+00:00WhyteboardBlog discussing the development of Whyteboard - an open source drawing applicationSteven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.comBlogger21125tag:blogger.com,1999:blog-8337871404439478979.post-88756974232614147332012-11-24T19:54:00.001+00:002012-11-24T19:54:04.744+00:00Constant loggingIn the upcoming 1.0 release of Whyteboard, there is substantial logging across the program. This can help track down bugs when users report crashes from the program's internal error catching dialog. Whyteboard can now submit a log of the user's actions leading up to the crash as well as the stack trace.
I needed the program to operate in several view states: it's always in debug mode but
the debugging may not always be visible. I had to create a custom log handler to allow me to 'save' every log message in order to attach it to the error email.
<pre class="brush: python">
from collections import deque
from logging import StreamHandler, DEBUG
from time import gmtime, strftime
class LogRemembererHandler(StreamHandler):
'''
A custom log handler that remembers the last x many logs sent to it
for retrieving log messages (in this case, we want to fetch the logs
if the program crashes)
'''
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(LogRemembererHandler, cls).__new__(cls, *args, **kwargs)
cls._instance.logs = deque(maxlen=101)
return cls._instance
def emit(self, record):
self.format(record)
time = strftime("%d/%m/%Y %H:%M:%S", gmtime(record.created))
self.logs.append(u"%s: %s" % (time, record.message))
def get_logs(self):
return self.logs
</pre>
and to use:
<pre class="brush: python">
formatter = logging.Formatter('%(levelname)s %(asctime)s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
...
LogRemembererHandler().get_logs()
</pre>
Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-57655268129947326752012-04-19T00:52:00.004+01:002012-04-19T01:01:03.077+01:00Just do itI've started a new job as an ASP.NET developer. The development process here is much different to my previous job - no unit tests, all logic crammed into the aspx.vb files, huge methods, no ORM, ad-hoc release process and such. <br /><br />Thankfully they use version control, however no bug database. I've only been working for 3 days and, while having done no coding I feel that I work better with issue tracking already, especially in a team. The current flow is bug tracking via email or verbally. I've been looking around and found that Fogbugz offers an On Demand service that lets you run Fogbugz on their servers for a free 45 day period.<br /><br />In an attempt to win over my bosses to issue tracking I'm simply going to use it to track all my work. Any existing issues I come across on our sites will be logged and I may create a per-project milestone for tracking bits of code to refactor.<br /><br />In regards to unit testing, I'll probably do something similar to a Visual Basic ASP.NET project at my old company where I extract our page logic into a separate DLL that can be unit tested independently, and the aspx.vb pages act as simple controllers that updates the view. I actually like this approach and don't mind doing much view-related programming in these controllers - I like the integration of control IDs to typed variables in the code-behind code.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-27681246193040596462011-11-17T22:59:00.002+00:002011-11-17T23:01:40.657+00:00Be wary of config settings and public source codeI forgot that a few weeks back I made a commit to Whyteboard's build procedure, that uploads a newly-created file via FTP a website, to allow Whyteboard to check if a new version has been released. <br /><br />During this commit, I committed a file with my hosting username and password. Silly man!Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-88230086592821659382011-10-29T17:59:00.002+01:002011-10-29T19:40:19.940+01:00Automating the build and release procedureIn the past I have been frustrated at the amount of time it takes for me to create a build, due to manual tasks that needed to be performed. I have finally taken a step towards an automated build process by creating a series of .bat and .sh scripts for Windows and Linux that perform the boring work for me.<br /><br />There are a number of scripts, each performing different 'stages' of the build. e.g. on Windows there is build.bat, binaries.bat and release.bat. <br /><br />Build: Creates the .exe from the Python source and includes all distributable resources etc that are needed into a folder.<br /><br />Binaries: First, it asks for a version number. It then modifies the program's meta script that defines the version number to ensure it's correctly set. <br />It then calls the build script above, and then creates an installer file by launching InnoSetup as a GUI-less program. It then creates .zip files for the stand-alone exe and moves the files to a directory ready for release.<br /><br />Release: It updates the file that is used by the program to look up the latest version number when doing an update check. It then FTPs this to the server.<br /><br /><br />I've added the option for Pylint to be run against all the program's codebase and output to a file; I'm figuring out how to get my unit tests running from the command line too. I'm looking into whether I can release the binaries to the several sites where I host the program via a REST API or over FTP. This would be a huge improvement.<br /><br />All in all, this will help me deploy changes much quicker when releasing.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-8674218347420534962011-06-10T21:48:00.006+01:002011-06-10T21:52:58.627+01:00Logging - simple mistakeI recently ran into a problem where my logging started to duplicate each log message, but in a different format after one particular logging call.<br /><br />I could not track down the error, until I actually read what I wrote:<br /><br /><pre class="brush: python"><br />import logging<br /><br />logger = logging.getLogger("a.logger")<br /><br />class Something:<br /> ...<br /> logging.debug("a message")<br /></pre><br /><br />I'd accidentally called debug on the logging module, not my logger. Everything I was googling was pointing towards enabling the default logging config (e.g. <pre style="display: inline;">logging.basicConfig()</pre>) but I hadn't done so.<br /><br />Easy mistake to make, thought I'd briefly write about it.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com1tag:blogger.com,1999:blog-8337871404439478979.post-59078695642697003492011-06-10T12:11:00.004+01:002011-06-10T12:15:26.635+01:00Adding logging to WhyteboardI've been undertaking quite a large change to the program: logging. Now, the program prints out a log of debug, information and warning logs to help see what's going on. I'm perhaps 25-30% complete in adding the logging, and it's already helping immensely. Just when loading a file for example, I can now see the steps the program takes in figuring out whether to convert a PDF, load a .wtbd file and then the step-by-step actions being performed there. It's great<br /><br /><br />I'm just not too sure about the best strategy for debugging things like events - e.g. in the mouse motion event handler, which gets called hundreds of times for the Pen tool, we don't want to flood the console. <br /><br />Also, not too sure about what to do with logging the debug messages to file by default? One of the whole points to all of this is that I can view a user's debug log to see what they did before triggering some exception in an error report.<br /><br />hmm.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-91184000551621049732010-11-11T01:04:00.002+00:002010-11-11T01:06:58.645+00:00Article on Whyteboard in Dec 2010 Linux JournalAn article has been published in Issue 200, December 2010 of the Linux Journal magazine, in the "new projects" section. Praise was given for its simple UI, PDF annotating abilities, the history replay view and being able to embed multimedia into a sheet.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com2tag:blogger.com,1999:blog-8337871404439478979.post-59367320734386404312010-09-15T23:36:00.002+01:002010-09-15T23:39:14.141+01:00Having a simple websiteThe Whyteboard <a href="http://whyteboard.org">home page is simple</a>. Very simple. I was going to create a "modern" site, with some nice javascript goodness and whatnot, but I have trouble creating a website from blank. I'm not much of a design guy - I do a little CSS at work, but not in a few months, and it's always been making changes to existing sites. <br /><br />So, I've gone the complete opposite and done the most basic site I could. It's pretty much one page with some summarised information about the program.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-16933243473391225162010-08-31T20:12:00.002+01:002010-08-31T20:17:24.863+01:00Whyteboard 0.41 released!Hurrah - finally got a release by the end of the month as I was hoping to do.<br /><br />This version should finally kill off all unicode related bugs - in total, over 15 bugs have been fixed. I'm embarrassed that they slipped through my radar - but at least they're now fixed.<br /><br /><span style="font-weight:bold;">What's new in this release?</span><br /><br />- Program now reads in your system language at startup and sets the default locale based on that. No more setting preferences!<br />- Many enhancements to the "Shape Viewer" dialog<br />- Misc. UI improvements such as little * shown in the program title when there's unsaved changes.<br /><br />- Massive code overhauls and changes to make the program easier to develop on<br /><br /><br /><br />Wow! Looking back it seems like there's not much new...<br /><br />Download at <a href="http://code.google.com/p/whyteboard/downloads/list">http://code.google.com/p/whyteboard/downloads/list</a>Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-67498626865108934692010-07-31T16:24:00.002+01:002010-07-31T16:27:47.067+01:00Packaging her upI've decided to try and package my classes into more appropriate locations instead of containing everything into one directory. I've just committed a <a href="http://bazaar.launchpad.net/~sproaty/whyteboard/development/revision/319">repackaging of my GUI classes</a>. I'd like to go further and split each file into smaller modules, with one classes each, especially for my "Tools".<br /><br />However doing this will definitely break older save files and I'll have to monkey patch around it. I think I should move away from using Pickle as my save format, and perhaps use JSON instead. I just need to find a way to make a save converter for existing files...Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-75080678864682965792010-07-22T01:10:00.003+01:002010-07-22T01:14:30.594+01:00Refactoring, refactoringI've spent some time refactoring the program's code lately. The GUI class is getting really bloated with features and functionality that should be handled by a separate class. Thus, I've created a Menu class to create, bind and query the menu. Pretty good...<br /><br />Also in the utility class, my save method was growing to over 100 lines, with many temporary variables being used. I managed to extract this into its own class, and split up the method into several smaller ones, passing parameters between them as needed. My temporary variables from the giant class became instance variables in the extracted class. <br /><br />The end result is more code, but ultimately an easier to read, more abstract higher level save method - you just read the method calls it makes, as opposed to having to iterate over objects, save values etc inside the method.<br /><br />I need to apply this to many more places in the program. I want to refactor out tab control to its own class, so that it can be shared by the Notes, thumbs and gui classes; these all share common data but it's duplicated per-class. Also the code is really hard to test - refactoring it out will definitely help.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-80778492951775359662010-07-13T02:24:00.007+01:002010-07-13T02:49:01.935+01:00Understanding Python decoratorsI've finally come to grasp with Python's decorators, and have managed to create somewhat nicer code. Here's what I had before:<br /><br /><pre class="brush: python"><br /> def on_top(self, event):<br /> index, item = self.find_shape()<br /> self.shapes.pop(index)<br /> self.shapes.append(item)<br /> self.populate()<br /> self.list.Select(0)<br /><br /><br /> def on_bottom(self, event):<br /> index, item = self.find_shape()<br /> self.shapes.pop(index)<br /> self.shapes.insert(0, item)<br /> self.populate()<br /> self.list.Select(len(self.shapes) - 1)<br /><br /><br /> def on_up(self, event):<br /> index, item = self.find_shape()<br /> self.shapes.pop(index)<br /> self.shapes.insert(index + 1, item)<br /> x = self.list.GetFirstSelected() - 1<br /> self.populate()<br /> self.list.Select(x)<br /><br /><br /> def on_down(self, event):<br /> index, item = self.find_shape()<br /> self.shapes.pop(index)<br /> self.shapes.insert(index - 1, item)<br /> x = self.list.GetFirstSelected() + 1<br /> self.populate()<br /> self.list.Select(x)<br /></pre><br /><br /><br />As you can see - a lot of repeated code, with only different things happening in the middle of each function. This is ideal for a decorator. I struggled for a while to get it working as a decorator function inside a class, before reading how you define a wrapper function for creating a closure over the decorated function.<br /><br />My code now reads, <br /><br /><pre class="brush: python"><br /> def move_shape(fn):<br /> """<br /> Passes the selected shape and its index to the decorated function, which<br /> handles moving the shape. function returns the list's index to select<br /> """<br /> def wrapper(self, event, index=None, item=None):<br /> index, item = self.find_shape()<br /> self.shapes.pop(index)<br /> x = fn(self, event, index, item)<br /><br /> self.populate()<br /> self.list.Select(x)<br /> return wrapper<br /><br /> @move_shape<br /> def on_top(self, event, index=None, item=None):<br /> self.shapes.append(item)<br /> return 0<br /><br /> @move_shape<br /> def on_bottom(self, event, index=None, item=None):<br /> self.shapes.insert(0, item)<br /> return len(self.shapes) - 1<br /><br /> @move_shape<br /> def on_up(self, event, index=None, item=None):<br /> self.shapes.insert(index + 1, item)<br /> return self.list.GetFirstSelected() - 1<br /><br /> @move_shape<br /> def on_down(self, event, index=None, item=None):<br /> self.shapes.insert(index - 1, item)<br /> return self.list.GetFirstSelected() + 1<br /></pre><br /><br />Unfortunately the event parameter is there because these are also wxPython event listeners, and I'd have to create delegate functions just to remove the one parameter, which seems pointless.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-29599275709157178802010-06-25T01:09:00.002+01:002010-06-25T01:15:53.819+01:00More pubsub calls<a href="http://pubsub.sourceforge.net/">PubSub</a> is a great Python package allowing you to decouple your objects from one another. I've changed Whyteboard to use more pubsub message passing instead of direct method invocation; it's allowed me to test the application easier and to also chain multiple actions from a single message (e.g. "change tab" updates the thumbnail, bolds its text and changes the notes' tree control selection<br /><br />Eventually none of my objects will know about any of its interaction objects, except for the GUI, which will mediate method calls. Perhaps I could even put this mediator outside of the GUI so that the GUI only creates dialogs and other GUI components and handles menus' events by delegating it to another object<br /><br /><br />My main problem is the models sometimes need to know the state of the GUI or its sub-components: e.g. the canvas may wish to know if the drawing mode is set to transparent - which is stored in gui.util.<br /><br />Eventually the canvas will have no reference to the GUI at all, so it needs to know a way to query the current state of the program. Hmm...Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-83410696733217122132010-05-18T00:35:00.002+01:002010-05-18T00:37:43.268+01:00Note: test before releaseEmbarrassing...released version 0.40 with about 6 bugs that I found within a 10 minute play around with the program. For shame!<br /><br />Anyway, that takes care of that, I hope. Version <span style="font-weight:bold;">0.40.1</span> released Had another bug report about damn unicode errors - thought I'd fixed those! oh dearSteven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-35454245350025796952010-05-16T22:15:00.001+01:002010-05-16T22:29:26.287+01:00Whyteboard 0.4 releasedNew whyteboard released, bringing a whole new bunch of features.<br /><br />New features:<br />- Highlighter tool<br />- Improvements to selection tool<br />- UI changes<br />- Better shape viewer<br />- PDF Cache viewer<br />- bug fixes and misc. changes/improvements - see changelog for further details<br /><br />Enjoy. As always, report any bugs/issues/suggestions.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-76254812621460935732010-05-03T00:25:00.003+01:002010-07-13T02:55:01.609+01:00The Law of DemeterI've been learning a lot about refactoring over the past few days, and have made an effort to put these practices into effect. One interesting concept is the <a href="http://en.wikipedia.org/wiki/Law_of_Demeter">Law of Demeter</a> - classes should know as little about their interacting classes as possible. This means restricting access on instance variable, and method arguments.<br /><br />Suppose we have an instance variable of type B inside A. Class B contains a reference to class C. The Law of Demeter states we should not access properties of C through B; instead we should tell B on how to interact with C.<br /><br />Here's a before/after example of some code from Whyteboard I've modified, involving selecting a shape using the Select tool:<br /><br /><pre class="brush: python"><br /><br />class Select:<br /> def check_for_hit(self, shape, x, y):<br /> """<br /> Sees if a shape is underneath the mouse coords, and allows the shape to<br /> be re-dragged to place<br /> """<br /> found = False<br /> handle = shape.handle_hit_test(x, y) # test handle before area<br /><br /> if handle:<br /> self.handle = handle<br /> found = True<br /> elif shape.hit_test(x, y):<br /> found = True<br /><br /> if found:<br /> self.canvas.overlay = wx.Overlay()<br /> self.shape = shape<br /> self.dragging = True<br /> self.offset = self.shape.offset(x, y)<br /><br /> if self.canvas.selected:<br /> self.canvas.deselect()<br /> self.canvas.selected = shape<br /> shape.selected = True<br /><br /> pub.sendMessage('shape.selected', shape=shape)<br /> return found<br /><br /><br />class Gui:<br /> # bind a handler for shape.selected<br /> pub.subscribe(self.shape_selected, 'shape.selected')<br /><br /> def shape_selected(self, shape):<br /> """<br /> Shape getting selected (by Select tool)<br /> """<br /> x = self.canvas.shapes.index(shape)<br /> self.canvas.shapes.pop(x)<br /> self.canvas.redraw_all() # hide 'original'<br /> self.canvas.shapes.insert(x, shape)<br /> shape.draw(self.canvas.get_dc(), False) # draw 'new'<br /><br /> ctrl, menu = True, True<br /> if not shape.background == wx.TRANSPARENT:<br /> ctrl, menu = False, False<br /><br /> self.control.transparent.SetValue(ctrl)<br /> self.menu.Check(ID_TRANSPARENT, menu)<br /></pre><br /><br />In this code, we see the Select tool (a "model" is manipulating the canvas directly, when it really shouldn't be doing so. We can delegate this to the canvas itself, through the GUI.<br /><br /><br /><pre class="brush: python"><br />class Select:<br /> def check_for_hit(self, shape, x, y):<br /> """<br /> Sees if a shape is underneath the mouse coords, and allows the shape to<br /> be re-dragged to place<br /> """<br /> found = False<br /> handle = shape.handle_hit_test(x, y) # test handle before area<br /><br /> if handle:<br /> self.handle = handle<br /> found = True<br /> elif shape.hit_test(x, y):<br /> found = True<br /><br /> if found:<br /> self.shape = shape<br /> self.dragging = True<br /> self.offset = self.shape.offset(x, y)<br /> pub.sendMessage('shape.selected', shape=shape)<br /> return found<br /><br /><br />class Gui:<br /> # bind a handler for shape.selected<br /> pub.subscribe(self.shape_selected, 'shape.selected')<br /><br /> def shape_selected(self, shape):<br /> """<br /> Shape getting selected (by Select tool)<br /> """<br /> self.canvas.select_shape(shape)<br /><br /> ctrl, menu = True, True<br /> if not shape.background == wx.TRANSPARENT:<br /> ctrl, menu = False, False<br /><br /> self.control.transparent.SetValue(ctrl)<br /> self.menu.Check(ID_TRANSPARENT, menu)<br /><br /><br />class Canvas:<br /> def select_shape(self, shape):<br /> """Selects the selected shape"""<br /> self.overlay = wx.Overlay()<br /> if self.selected:<br /> self.deselect_shape()<br /><br /> self.selected = shape<br /> shape.selected = True<br /> x = self.shapes.index(shape)<br /> self.shapes.pop(x)<br /> self.redraw_all() # hide 'original'<br /> self.shapes.insert(x, shape)<br /> shape.draw(self.get_dc(), False) # draw 'new'<br /></pre><br /><br /><br />We can now see that the appropriate classes are performing their correct responsibilities. The model sends out a message to indicate something's changed, without caring how the event is handled, and the GUI is updating its controls and menus. The GUI delegates a method call to the canvas which updates itself.<br /><br />There are no longer many "access levels" (this.that.other.do_something) as all operations are on variables within a small scope. This is better code that's easier to test.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-87490557105664165702010-04-21T09:41:00.003+01:002010-04-21T11:47:04.855+01:00New release almost readySince the last post, development has steadily resumed again. Over 30 commits have been made, fixing bugs and adding in a bunch of new features. One of the new things I've addded is when you are moving a shape using the Select tool, moving the shape close to the canvas' edge will scroll the canvas in that direction, so that you don't have to stop dragging the shape, scroll the canvas manually and resume dragging.<br /><br />There's also a new feature that allows you to move selected shapes using the keyboard arrow keys. This integrates nicely with the above "canvas edge" auto-scroll. The shape viewer has gained a "delete" button to remove un-selectable shapes such as a pen/eraser. <br /><br />The shape viewer has gained several improvements too, such as better synchronisation with the program as a whole. Undo/redo/deleting shapes/renaming a sheet all trigger the viewer to update itself and shapes report their properties more accurately.<br /><br />A PDF Cache viewer allows you to see cached pdf/svg/ps files, and remove them from the cache, forcing Whyteboard to re-convert the item. The date a file is converted is also stored into the cache so you know when it was last converted.<br /><br />Recently Closed Sheets is implemented as a sub-menu - as well as being able to re-open the last closed sheet with Ctrl+Shift+T, you can browse through the sub-menu and choose which sheet to close.<br /><br />and various other small tweaks/improvements.<br />Stay tuned for a release - there's only a few bugs to iron out now. There's been over 80 commits between the last release and the current one - a lot has changed, making the program much better, overall.<br /><br />Here's 2 HD videos showing some new stuff:<br /><br /><span style="font-weight:bold;">Improved Shape Viewer</span><br /><br /><object width="980" height="765"><param name="movie" value="http://www.youtube.com/v/VRQZeunda7M&hl=en_GB&fs=1&rel=0&hd=1&border=1"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/VRQZeunda7M&hl=en_GB&fs=1&rel=0&hd=1&border=1" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="980" height="765"></embed></object><br /><br /><br /><span style="font-weight:bold;">Shapes Scrolling the canvas</span><br /><br /><object width="980" height="765"><param name="movie" value="http://www.youtube.com/v/vqcv3g_tWd0&hl=en_GB&fs=1&rel=0&hd=1&border=1"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/vqcv3g_tWd0&hl=en_GB&fs=1&rel=0&hd=1&border=1" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="980" height="765"></embed></object>Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-28417452271107349052010-03-27T16:01:00.001+00:002010-03-27T16:02:49.487+00:00Arrested DevelopmentDevelopment has practically stopped lately. Been busy with work - when I get home I really don't want to spend time programming.<br /><br />I plan on getting a bit of work done this weekend - it's been too long since I coded!Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-41958872429026823502010-02-01T20:01:00.003+00:002010-02-01T20:11:26.467+00:00Finally EmployedWell, it happened. Seven months after graduating university I've secured myself a web programming job using a variety of new technologies and development methodologies. I've only previously read about most of these, e.g. Agile, Groovy, Spring/Hibernate - which I told them in the interview, and was told that they weren't looking for someone with previous experience. <br /><br />Now, I have 2 weeks until I start to learn as much about these things as I can just to get a nice head start into development, and make my first few weeks a little easier because I won't be starting from scratch.<br /><br />Obviously this means that time spent to develop on Whyteboard will become limited; working full-time as a programmer, I may not *want* to program much in my spare time. But who knows, I do enjoy development and I'm definitely not putting the project on hold. Progress will continue, but slowly.<br /><br />Goals: release Whyteboard 0.39.4 in the next 2 weeks. Learn Groovy, Grails and spend some time unit testing Whyteboard more.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-55144812195097014752010-01-31T10:05:00.006+00:002010-07-13T02:55:27.181+01:00Lack of Testing: An ExampleIn my previous post I was saying how it's hard to test my code independently of the GUI Framework, since my application is heavily dependant upon the services it provides. Here's an example, exporting an image (which is represented in my program as a buffer that is drawn to)<br /><br /><br />Here's the basics:<br />- prompt user for location to export to.<br />- display a filedialog containing a file filter for image types<br />- check whether the given filetype is acceptable<br />- if there is no filetype present, append the currently selected file filter file extension<br />- save the data<br /><br /><pre class="brush: python"><br /> def on_export(self, event=None):<br /> """Exports the current sheet as an image, or all as a PDF."""<br /> filename = self.export_prompt()<br /> if filename:<br /> self.util.export(filename)<br /><br /><br /> def export_prompt(self):<br /> """Returns the filename to save to"""<br /> val = None # return value<br /> wc = ("PNG (*.png)|*.png|JPEG (*.jpg, *.jpeg)|*.jpeg;*.jpg|" +<br /> "BMP (*.bmp)|*.bmp|TIFF (*.tiff)|*.tiff")<br /><br /> dlg = wx.FileDialog(self, _("Export data to..."), style=wx.SAVE |<br /> wx.OVERWRITE_PROMPT, wildcard=wc)<br /> if dlg.ShowModal() == wx.ID_OK:<br /> filename = dlg.GetPath()<br /> _name = os.path.splitext(filename)[1].replace(".", "")<br /> types = {0: "png", 1: "jpg", 2: "bmp", 3: "tiff"}<br /><br /> if not os.path.splitext(filename)[1]: # no extension<br /> _name = types[dlg.GetFilterIndex()]<br /> filename += "." + _name<br /> val = filename<br /> if not _name in meta.types[2:]:<br /> wx.MessageBox(_("Invalid filetype to export as:")+" .%s" % _name,<br /> _("Invalid filetype"))<br /> else:<br /> val = filename<br /><br /> dlg.Destroy()<br /> return val<br /><br /><br />#------ the util.export() method:<br /><br /><br /> def export(self, filename):<br /> const = get_wx_image_type(filename)<br /> self.gui.board.deselect()<br /> self.gui.board.redraw_all()<br /><br /> context = wx.MemoryDC(self.gui.board.buffer)<br /> memory = wx.MemoryDC()<br /> x, y = self.gui.board.buffer.GetSize()<br /> bitmap = wx.EmptyBitmap(x, y, -1)<br /> memory.SelectObject(bitmap)<br /> memory.Blit(0, 0, x, y, context, 0, 0)<br /> memory.SelectObject(wx.NullBitmap)<br /> bitmap.SaveFile(filename, const) # write to disk<br /><br /><br />def get_wx_image_type(filename):<br /> """<br /> Returns the wx.BITMAP_TYPE_X for a given filename<br /> """<br /> _name = os.path.splitext(filename)[1].replace(".", "").lower()<br /><br /> types = {"png": wx.BITMAP_TYPE_PNG, "jpg": wx.BITMAP_TYPE_JPEG, "jpeg":<br /> wx.BITMAP_TYPE_JPEG, "bmp": wx.BITMAP_TYPE_BMP, "tiff":<br /> wx.BITMAP_TYPE_TIF, "pcx": wx.BITMAP_TYPE_PCX }<br /><br /> return types[_name] # grab the right image type from dict. above<br /></pre><br /><br />meta.types is a Python list of valid filetypes, e.g. ['jpg', 'png', 'bmp'...]<br /><br />Now, sure, I could go in an test that given a particular filetype, the MessageBox is displayed (or not), or that entering no file name appends the correct extension. <br /><br />The easiest way for me to test this is to draw a bunch of lines, and then try exporting it. I verify the file dialog appears, the file type filter works. I try selecting PNG, and typing in test. I see a file named test.png is created, I open it, it matches my image. I repeat the process, typing test2.png. File test2.png is created.<br /><br />Writing mock GUI dialogs to return fake values and doing this in a TDD manner just seems like it wouldn't have helped much. I know there's a bit of code repeat (in get_wx_image_type) and this may not be the best example.Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0tag:blogger.com,1999:blog-8337871404439478979.post-58018514985963989232010-01-31T09:28:00.009+00:002010-02-01T20:11:10.226+00:00On testing, and attempting test-driven developmentI want to unit test. I really do. I find it pretty hard to start with adding a bunch of tests to existing code that you know is working, to some extent. My application relies heavily on the GUI, and the GUI itself cannot be tested, and has to be mocked with fake classes returning mock data.<br /><br />This of course has its downsides. I've found myself just writing my program's code in an ad-hoc manner, with some thought as to what I want to achieve, but not necessarily how I'm going to get there. I'll try some things that seem to work, and begin building around this. I notice common code and refactor it into more readable, less copy/pasted methods.<br /><br />But, I still run into issues from time to time. My code's main problem is its tight object coupling -- what should be my "Model"s know about their Controller/View, and explicitly call methods on them. I'm slowly changing this by using the Publish/Subscribe pattern, where a message is broadcast through my system and is handled by any interested listeners, instead of calling methods directly on the controllers.<br /><br />But, this is going to take a long time to do. I want to try test-driven development by building up a feature of code with unit tests proving that the code does what it should. However, this comes with complexity - if I want to edit the code that saves the program's data to disk, then it's hard to test if valid data is being created. Or, if I wish to test that my new drawing tool actually draws correctly.<br /><br />How can a unit test verify this? The simplest way (for me) is to try drawing with the tool. I added a bug reporter to Whyteboard in October 2009 and most of the reports I've received are issues that I would have never caught with unit tests -- broken/misconfigured wxPython installations, Mac-specific issues, Unicode errors, problems with the Media player. <br /><br />These all relate to environment, and as a sole developer on a limited budget (I can't test on a Mac!) there is pretty much nothing I can do but to speculate what may be causing errors based on limited data I have. <br /><br />Oh well, I've totally gone off the subject of this post. I will give TDD a shot, just to see how beneficial it is. But, my program needs a change to become more testable. Making these changes will undoubtedly introduce regressions that will go undetected due to the lack of current tests!Steven Sproathttp://www.blogger.com/profile/07781153580072642969noreply@blogger.com0