<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:admin="http://webns.net/mvcb/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:html="http://www.w3.org/1999/html" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>Random notes from mg</title><link>http://mg.pov.lt/blog</link><description>a blog by Marius Gedminas</description><language>en</language><ttl>60</ttl><dc:creator>Marius Gedminas</dc:creator><admin:generatorAgent rdf:resource="http://pyblosxom.sourceforge.net/"/><admin:errorReportsTo rdf:resource="mailto:marius@gedmin.as"/><item><title>Logging levels and hierarchies</title><guid isPermaLink="false">logging-levels</guid><link>http://mg.pov.lt/blog/logging-levels</link><description>I remember when the logging package seemed big and complicated and forbidding. And then I remember when I finally &quot;got&quot; ...</description><content:encoded><![CDATA[
<p>I remember when the logging package seemed big and complicated and
forbidding.  And then I remember when I finally "got" it, started using it,
even liked it.  And then I've discovered that I didn't really understand
the model after all.</p>

<p>Consider this: we have two loggers</p>
<blockquote>
<pre>
root
  \
   -- mylogger
</pre>
</blockquote>
<p>configured as follows:</p>
<blockquote>
<pre>
<span class="keyword">import</span> <span class="name">logging</span>
<span class="name">root</span> = <span class="name">logging</span>.<span class="name">getLogger</span>()
<span class="name">root</span>.<span class="name">setLevel</span>(<span class="name">logging</span>.<span class="name">INFO</span>)
<span class="name">root</span>.<span class="name">addHandler</span>(<span class="name">logging</span>.<span class="name">FileHandler</span>(<span class="string">"info.log"</span>))
<span class="name">mylogger</span> = <span class="name">logging</span>.<span class="name">getLogger</span>(<span class="string">'mylogger'</span>)
<span class="name">mylogger</span>.<span class="name">setLevel</span>(<span class="name">logging</span>.<span class="name">DEBUG</span>)
<span class="name">mylogger</span>.<span class="name">addHandler</span>(<span class="name">logging</span>.<span class="name">FileHandler</span>(<span class="string">"debug.log"</span>))
</pre>
</blockquote>
<p>What happens when I do <code>mylogger.debug('Hi')</code>?</p>

<p>Answer: the message appears in both debug.log and info.log.</p>

<p>That was surprising to me.  I'd always thought that a logger's level was
a gatekeeper to all of that logger's handlers.  In other words, I always
thought that when a message was propagating from a logger to its parent (here
from mylogger to root), it was also being filtered against the parent's log
level. That turns out not to be the case.</p>

<p>What actually happens is that a message is tested against the level of
the logger where it was initially logged, and if it passes the check, it
gets passed to all the handlers of that logger and all its ancestors with no
further checks.  Unless the propagation is stopped somewhere by one of the
loggers having propagate set to False.  And of course each handler has its own
level filtering.  And I'm ignoring <a href="http://docs.python.org/library/logging.html#filter-objects">filters</a>
and the <a href="http://docs.python.org/library/logging.html#logging.disable">global level override</a>.</p>

<p>Part of the confusion was caused by my misunderstanding of
<code>logging.NOTSET</code>.  I assumed, incorrectly, that it was just a
regular logging level, one even less severe than <code>DEBUG</code>.  So when I
wrote code like this: </p>

<blockquote>
<pre>
<span class="keyword">import</span> <span class="name">logging</span>
<span class="name">root</span> = <span class="name">logging</span>.<span class="name">getLogger</span>()
<span class="name">root</span>.<span class="name">setLevel</span>(<span class="name">logging</span>.<span class="name">INFO</span>)
<span class="name">root</span>.<span class="name">addHandler</span>(<span class="name">logging</span>.<span class="name">FileHandler</span>(<span class="string">"info.log"</span>))
<span class="name">mylogger</span> = <span class="name">logging</span>.<span class="name">getLogger</span>(<span class="string">'mylogger'</span>)
<span class="name">mylogger</span>.<span class="name">setLevel</span>(<span class="name">logging</span>.<span class="name">NOTSET</span>)  <span class="comment"># &lt;-- that's the default level, actually</span>
<span class="name">mylogger</span>.<span class="name">debug</span>(<span class="string">"debug message"</span>)
</pre>
</blockquote>

<p>I saw the debug message being suppressed and assumed it was because of the
root logger's level.  Which is correct, in a way, just not the way I thought
about it.</p>

<p><code>NOTSET</code> does not mean "pass all messages through", it
means "inherit the log level from the parent logger".  The documentation
actually <a href="http://docs.python.org/library/logging.html#logging.Logger.setLevel">describes</a>
this, although in a rather convoluted way.  My own fault for misunderstanding
it, I guess.</p>
]]></content:encoded><dc:date>2012-04-12T15:01:56Z</dc:date></item><item><title>trace.py and os.fork()</title><guid isPermaLink="false">trace-and-fork</guid><link>http://mg.pov.lt/blog/trace-and-fork</link><description>Here's a simple Python script: import subprocess with open ( 'hi.txt' , 'w' ) as f : subprocess . Popen ...</description><content:encoded><![CDATA[
<p>Here's a simple Python script:</p>

<blockquote><pre>
<span class="keyword">import</span> <span class="name">subprocess</span>
<span class="keyword">with</span> <span class="name">open</span>(<span class="string">'hi.txt'</span>, <span class="string">'w'</span>) <span class="keyword">as</span> <span class="name">f</span>:
  <span class="name">subprocess</span>.<span class="name">Popen</span>([<span class="string">'echo'</span>, <span class="string">'Hello'</span>], <span class="name">stdout</span>=<span class="name">f</span>).<span class="name">wait</span>()
</pre></blockquote>

<p>Here's what it does:</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">python2.7 hi.py</span>
<span class="prompt">$</span> <span class="typing">cat hi.txt</span>
Hello
</pre></blockquote>

<p>And here's what happens if you try to use the <a
href="http://docs.python.org/library/trace">trace</a> module to trace the
execution:</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">python2.7 -m trace --trace hi.py</span>
(lots of trace output omitted)
</pre></blockquote>

<p>So far so good, but take a look at hi.txt:</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">cat hi.txt</span>
subprocess.py(1170):                             _dup2(errwrite, 2)
 --- modulename: subprocess, funcname: _dup2
subprocess.py(1164):                                 if a == b:
subprocess.py(1166):                                 elif a is not None:
<em>... 154 lines of unexpected trace output omitted ...</em>
os.py(379):         try:
os.py(380):             func(fullname, *argrest)
Hello
</pre></blockquote>

<p>The tracing hook installed by trace.py survives the os.fork() and interferes
with subprocess output redirection.  Now you know.</p>

<p>Imagine what this does to scripts that execute shell commands and try to
parse their output.</p>

<p>Who wants to <a href="http://bugs.python.org/issue?@template=item">file a
  bug</a> and/or provide a patch?  I'm afraid I don't have enough round
tuits...</p>
]]></content:encoded><dc:date>2012-03-18T19:15:19Z</dc:date></item><item><title>logging.config.fileConfig gotcha</title><guid isPermaLink="false">logging-fileconfig-gotcha</guid><link>http://mg.pov.lt/blog/logging-fileconfig-gotcha</link><description>If you use logging.config.fileConfig (e.g. because you use paster serve something.ini to deploy your WSGI apps) you should know about ...</description><content:encoded><![CDATA[
<p>If you use
<a href="http://docs.python.org/library/logging.config.html#logging.config.fileConfig">logging.config.fileConfig</a>
(e.g. because you use <tt>paster serve something.ini</tt> to deploy your WSGI
apps) you should know about this.</p>

<p>By default <tt>fileConfig</tt> <em>disables all pre-existing loggers</em>
if they (or their parent loggers) are not explicitly mentioned in your
<tt>.ini</tt> file.</p>

<p>This can result in unintuitive behaviour:</p>

<script src="https://gist.github.com/1642893.js"> </script>

<p>(if you don't see the embedded example, you can find it at <a
href="https://gist.github.com/1642893">https://gist.github.com/1642893</a>).</p>

<p>If you have Python 2.6 or later (and you should), you can turn this off
by passing <tt>disable_existing_loggers=False</tt> to <tt>fileConfig()</tt>.
But what if it's not you calling <tt>fileConfig()</tt> but your framework (e.g.
the above-mentioned <tt>paster serve</tt>)?</p>

<p>Now usually <tt>paster serve</tt> tries to configure logging before
importing any of your application modules, so there should be no pre-existing
loggers to disable.  Sometimes, however, this doesn't work for one reason or
another, and you end up with your production server suppressing warnings and
errors that should not be suppressed.  I haven't actually figured out yet who's
responsible for those early imports in the application I'm working on (until
today I assumed, incorrectly, that paster imports the module containing your
WSGI app before it calls <tt>fileConfig</tt>).
</p>

<p>If you're not sure if this bug can bite you or not, check that you don't
have any disabled loggers by doing something like</p>

<blockquote>
<pre>
<span class="keyword">import</span> <span class="name">logging</span>
<span class="keyword">assert</span> <span class="keyword">not</span> <span class="name">any</span>(<span class="name">getattr</span>(<span class="name">logger</span>, <span class="string">'disabled'</span>, <span class="name">False</span>)
               <span class="keyword">for</span> <span class="name">logger</span> <span class="keyword">in</span> <span class="name">logging</span>.<span class="name">getLogger</span>().<span class="name">manager</span>.<span class="name">loggerDict</span>.<span class="name">values</span>())
</pre>
</blockquote>

<p>while your application is running.</p>

]]></content:encoded><dc:date>2012-03-09T21:57:19Z</dc:date></item><item><title>Converting a gnarly SVN repository to GIT: success!</title><guid isPermaLink="false">eazysvn-git-migration-success</guid><link>http://mg.pov.lt/blog/eazysvn-git-migration-success</link><description>I've received more feedback about my last night's post on gnarly svn to git migration than I've expected. Thanks to ...</description><content:encoded><![CDATA[
<p>I've received more feedback about my last night's post on <a
href="http://mg.pov.lt/blog/eazysvn-git-migration.html">gnarly
svn to git migration</a> than I've expected.  Thanks to that feedback
(and, mostly, Raffaele Salmaso for doing almost all the work and emailing the
result to me) <a href="https://github.com/mgedmin/eazysvn">eazysvn is now on
  GitHub</a>.</p>

<p>The rest of this post will describe the conversion (and verification)
in detail, because if I ever need to do this again, I do not want to start
from scratch.</p>

<!--BREAK-->

<h4>Part one: unexpected gift</h4>

<p>Raffaele Salmaso did a heroic job and sent me a tarball with a mercurial
repository, produced with "something like" this:</p>

<blockquote><pre>
&gt; hg clone --layout single $SVN repo-tmp
&gt; hg convert --filemap filemap repo-tmp repo
&gt; cd repo
&gt; hg qinit
&gt; hg qimport -r 0:tip
&gt; hg qpop -a
&gt; cd .hg/patches
&gt; check patches for correctness
&gt; fix tags (svn are different from mercurial ones)
&gt; hg qfinish -a
</pre></blockquote>

<p>The conversion had only two problems:</p>

<ol>
  <li>it introduced new commits that modify a new file <tt>.htags</tt></li>
  <li>it changed the contents of README.txt in changeset 53 (corresponding to
      svn revision 55, "Allow branch names to have prefixes.") and newer
      versions:
  </li>
</ol>

<blockquote><pre>
--- svn version
+++ hg version
-  *scheme://server/path/to/svn/repo*/*subdirs*
+  *scheme://server/path/to/svn/repo*/trunk/*subdirs*
</pre></blockquote>

<p>
I did not notice either problem at first.
</p>

<h4>Part two: conversion to git</h4>

<p>
Note: I wrote this up <em>after</em> I've done everything up to and including
part five.  The commands and directory names here are not the actual commands
and directories I've used; although I tried to be accurate.  I've also skipped
some false trails.
</p>

<p>
Converting hg to git was pretty easy:
</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">mkdir -p /tmp/conv</span>
<span class="prompt">$</span> <span class="typing">cd /tmp/conv</span>
<span class="prompt">$</span> <span class="typing">tar xvjf eazysvn_20120220-133823.tar.bz2</span>
creates /tmp/conv/eazysvn/
<span class="prompt">$</span> <span class="typing">mkdir eazysvn-git</span>
<span class="prompt">$</span> <span class="typing">cd eazysvn-git</span>
<span class="prompt">$</span> <span class="typing">git init</span>
<span class="prompt">$</span> <span class="typing">hg-fast-export -r /tmp/conv/eazysvn</span>
converts, leaves no working tree; git status shows all files as deleted
<span class="prompt">$</span> <span class="typing">git checkout</span>
restore working tree
</pre></blockquote>

<p>I was a bit surprised by git status showing a bunch of deleted files at the
end there.  I suppose hg-fast-export expects to be run inside a bare
repository, or maybe it expects the user to know enough git to understand what
happened and do the <tt>git checkout</tt> if necessary.</p>

<h4>Part three: cleanup</h4>

<p>I wanted to drop the empty changesets that were introduced by Raffaele's
conversion process for modifying <tt>.hgtags</tt>.  Since the manual page for
git filter-branch had an example for dropping all empty changesets, I used it
directly.</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">git filter-branch --commit-filter 'git_commit_non_empty_tree "$@"'</span>
</pre></blockquote>

<p>This also dropped some changesets that were present in my Subversion
repository -- those that manipulated svn properties, and the one that moved
everything in svn root under /trunk.  I won't miss those.</p>

<p>Note: if you try</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">git filter-branch --commit-filter='git_commit_non_empty_tree "$@"'</span>
</pre></blockquote>

<p>(i.e. '<tt>=</tt>' instead of a space after <tt>--commit-filter</tt>), you will
get a completely baffling error message that doesn't even hint at what is
wrong.</p>

<p>At this point I ran <tt>gitk --all</tt> to look around and discovered that
git filter-branch left all the tags pointing to obsolete revisions.  I created
new tags manually with gitk, including some that were missing in my svn
repository.  Every release since 1.6.0 is now tagged (releases before that
did not have source tarballs on PyPI, so I had no way to verify which checkin
corresponded to which release).  I also changed the tag naming scheme to be
"v1.x.y" instead of just "1.x.y".</p>

<p>Oh, and to get rid of the old history I did</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">git tag -d 1.9.0 1.11.0 1.12.0 1.12.1</span>
<span class="prompt">$</span> <span class="typing">git gc --prune</span>
</pre></blockquote>

<h4>Part four: verification</h4>

<p>I downloaded all the available releases from PyPI:</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">mkdir -p /tmp/verify</span>
<span class="prompt">$</span> <span class="typing">cd /tmp/verify</span>
<span class="prompt">$</span> <span class="typing">for v in 1.6.0 1.6.1 1.7.0 1.8.0 1.9.0 1.10.0 1.11.0 1.12.0 1.12.1; do</span>
<span class="prompt">></span>   <span class="typing">wget http://pypi.python.org/packages/source/e/eazysvn/eazysvn-$v.tar.gz</span>
<span class="prompt">></span>   <span class="typing">tar xvzf eazysvn-$v.tar.gz</span>
<span class="prompt">></span> <span class="typing">done</span>
</pre></blockquote>

<p>Exported all of my git tags:</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">mkdir -p /tmp/verify/git</span>
<span class="prompt">$</span> <span class="typing">cd /tmp/conv/eazysvn-git</span>
<span class="prompt">$</span> <span class="typing">for v in 1.6.0 1.6.1 1.7.0 1.8.0 1.9.0 1.10.0 1.11.0 1.12.0 1.12.1; do</span>
<span class="prompt">></span>   <span class="typing">git archive --format=tar --prefix eazysvn-$v/ v$v | gzip \</span>
<span class="prompt">></span>       <span class="typing">> /tmp/verify/git/eazysvn-$v-git.tar.gz</span>
<span class="prompt">></span> <span class="typing">done</span>
<span class="prompt">$</span> <span class="typing">cd /tmp/verify/git</span>
<span class="prompt">$</span> <span class="typing">for v in 1.6.0 1.6.1 1.7.0 1.8.0 1.9.0 1.10.0 1.11.0 1.12.0 1.12.1; do</span>
<span class="prompt">></span>   <span class="typing">tar xvzf eazysvn-$v-git.tar.gz</span>
<span class="prompt">></span> <span class="typing">done</span>
</pre></blockquote>

<p>And compared them:</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">diff -ur /tmp/verify /tmp/verify/git</span>
</pre></blockquote>

<p>I expected to see "Only in dir1: setup.cfg" messages only for things like
'eazysvn.egg-info' or 'PKG-INFO'.  Unfortunately this is where actual
differences I mentioned in part one showed up: in README.txt for all trees
starting with release 1.9.0.</p>

<h4>Part five: rectification</h4>

<p>I needed to rewrite history again, but I didn't want to use git
filter-branch this time (I didn't want to manually tag all the releases
again).  I tried fast-export:</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">cd /tmp/conv/eazysvn-git</span>
<span class="prompt">$</span> <span class="typing">git fast-export --all > ../EXPORT.txt</span>
<span class="prompt">$</span> <span class="typing">vim ../EXPORT.txt</span>
search and replace 'repo*/*subdirs*' with 'repo*/trunk/*subdirs*'
fix up the file size above each change (increment by 6, the length of 'trunk/')
<span class="prompt">$</span> <span class="typing">mkdir /tmp/conv/eazysvn-git2</span>
<span class="prompt">$</span> <span class="typing">cd /tmp/conv/eazysvn-git2</span>
<span class="prompt">$</span> <span class="typing">git init</span>
<span class="prompt">$</span> <span class="typing">git fast-import &lt; ../EXPORT.txt</span>
succeeds, leaves no working tree; git status shows all files as deleted
<span class="prompt">$</span> <span class="typing">git checkout</span>
restore working tree
<span class="prompt">$</span> <span class="typing">gitk --all</span>
</pre></blockquote>

<p>Everything looked about right.</p>


<h4>Part six: final verification</h4>

<p>But was it actually right?</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">cd /tmp/verify/</span>
<span class="prompt">$</span> <span class="typing">mkdir pypi</span>
<span class="prompt">$</span> <span class="typing">mv eazysvn-*/ pypi/</span>
<span class="prompt">$</span> <span class="typing">rm -rf git/*</span>
<span class="prompt">$</span> <span class="typing">cd /tmp/conv/eazysvn-git2</span>
<span class="prompt">$</span> <span class="typing">for v in 1.6.0 1.6.1 1.7.0 1.8.0 1.9.0 1.10.0 1.11.0 1.12.0 1.12.1; do</span>
<span class="prompt">></span>   <span class="typing">git archive --format=tar --prefix eazysvn-$v/ v$v \</span>
<span class="prompt">></span>       <span class="typing">| (cd /tmp/verify/git && tar -xf - )</span>
<span class="prompt">></span> <span class="typing">done</span>
<span class="prompt">$</span> <span class="typing">cd /tmp/verify</span>
<span class="prompt">$</span> <span class="typing">diff -ur pypi git</span>
</pre></blockquote>

<p>Yes!</p>

<h4>Part seven: uploading to GitHub</h4>

<p>It was all plain sailing from here:</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">cd /tmp/conv/eazysvn-git2</span>
<span class="prompt">$</span> <span class="typing">git remote add origin git@github.com:mgedmin/eazysvn.git</span>
<span class="prompt">$</span> <span class="typing">git push -u origin master</span>
<span class="prompt">$</span> <span class="typing">git push --tags</span>
</pre></blockquote>

<p>And then there were documentation updates (to point to GitHub instead of the
old Subversion repository), Makefile updates (<tt>make release</tt> makes sure
my sdist contains everything I have in my repository, because I've been bitten
by setuptools magic before), etc.</p>

<p>I also released <a href="http://pypi.python.org/pypi/eazysvn">eazysvn
  1.12.2</a> to PyPI to test my Makefile changes, and because there were
unreleased changes that should've been released a long time ago.</p>

<p>So that's it.  Only took me three hours from the point where I found a
Mercurial repository in my inbox.</p>
]]></content:encoded><dc:date>2012-02-20T21:17:58Z</dc:date></item><item><title>Converting a gnarly SVN repository to GIT: FAIL.</title><guid isPermaLink="false">eazysvn-git-migration</guid><link>http://mg.pov.lt/blog/eazysvn-git-migration</link><description>eazysvn lives in a Subversion repository. I want to bring it (kicking and screaming) into the 21st century and put ...</description><content:encoded><![CDATA[
<p><a href="http://mg.pov.lt/eazysvn/">eazysvn</a> lives in a Subversion
repository.  I want to bring it (kicking and screaming) into the 21st century
and put it on Github.</p>

<p>git-svn is unsuitable for the conversion, because in revision 50 I moved
/ to /trunk and added the traditional /tags and /branches.  With git-svn I
either get one third of the history that ignores everything before the layout
switch, or I get directories named 'trunk', 'tags' and 'branches'.</p>

<p>Then I thought maybe hg would be smarter about the conversion, and then I
could use hg-fast-export to convert hg to git.  I enabled the hgsubversion
extension:</p>

<pre>
<span class="prompt">$</span> <span class="typing">sudo apt-get install hgsubversion</span>
<span class="prompt">$</span> <span class="typing">echo '[extensions'] >> ~/.hgrc</span>
<span class="prompt">$</span> <span class="typing">echo 'hgsubversion =' >> ~/.hgrc</span>
</pre>

<p>(blog posts like this are a good reason why hg ought to steal the 'git
config --global foo.bar=baz' syntax from git).</p>

<p>Then I converted the svn repository to Mercurial: </p>

<pre>
<span class="prompt">$</span> <span class="typing">hg clone svn+ssh://fridge/home/mg/svn/eazysvn eazysvn-hg</span>
</pre>

<p>hg log -p <del>confirmed that hg handled the conversion nicely</del> looked
right-ish at first glance, except the author information was nonsensical.  To
fix that:</p>

<pre>
<span class="prompt">$</span> <span class="typing">rm -rf eazysvn-hg</span>
<span class="prompt">$</span> <span class="typing">echo 'mg = Marius Gedminas &lt;marius@gedmin.as&gt;' > AUTHORS</span>
<span class="prompt">$</span> <span class="typing">hg clone svn+ssh://fridge/home/mg/svn/eazysvn eazysvn-hg -A AUTHORS</span>
</pre>

<p>Unfortunately, a closer look at hg log now shows that two thirds of the
history is lost: hg ignored everything before the layout restructuring, despite
printing the log messages of those revisions as it went about the conversion.
*sigh*.</p>

<p>Dear lazyweb, surely svn layout reorganization can't be such a rare thing
that no tools in existence support it?  What should I try next?</p>

<p>P.S. I also tried Bazaar, to see what it would do:</p>
<pre>
<span class="prompt">$</span> <span class="typing">bzr branch svn+ssh://fridge/home/mg/svn/eazysvn eazysvn-bzr</span>
Repository with UUID 4fc293c4-4eed-0310-a01a-b4ad72f90fad at svn+ssh://fridge/home/mg/svn/eazysvn contains fewer revisions than cache. This either means that this repository contains an out of date mirror of another repository (harmless), or that the UUID is being used for two different Subversion repositories (potential repository corruption).
bzr: ERROR: exceptions.KeyError: 'missing revision paths for 78'

Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/bzrlib/commands.py", line 946, in exception_to_return_code
    return the_callable(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/bzrlib/commands.py", line 1150, in run_bzr
    ret = run(*run_argv)
  File "/usr/lib/python2.7/dist-packages/bzrlib/commands.py", line 699, in run_argv_aliases
    return self.run(**all_cmd_args)
  File "/usr/lib/python2.7/dist-packages/bzrlib/commands.py", line 721, in run
    return self._operation.run_simple(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/bzrlib/cleanup.py", line 135, in run_simple
    self.cleanups, self.func, *args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/bzrlib/cleanup.py", line 165, in _do_with_cleanups
    result = func(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/bzrlib/builtins.py", line 1263, in run
    from_location)
  File "/usr/lib/python2.7/dist-packages/bzrlib/bzrdir.py", line 919, in open_tree_or_branch
    return bzrdir._get_tree_branch()
  File "/usr/lib/python2.7/dist-packages/bzrlib/controldir.py", line 410, in _get_tree_branch
    branch = self.open_branch(name=name)
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/remote.py", line 420, in open_branch
    branch_path = self._determine_relpath(name)
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/remote.py", line 369, in _determine_relpath
    layout = repos.get_layout()
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/repository.py", line 701, in get_layout
    return self.get_layout_source()[0]
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/repository.py", line 720, in get_layout_source
    self._find_guessed_layout(self.get_config())
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/repository.py", line 743, in _find_guessed_layout
    revnum, self._hinted_branch_path)
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/layout/guess.py", line 143, in repository_guess_layout
    return logwalker_guess_layout(repository._log, revnum, branch_path=branch_path)
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/layout/guess.py", line 149, in logwalker_guess_layout
    logwalker.iter_changes(None, revnum, max(0, revnum-GUESS_SAMPLE_SIZE)), revnum, branch_path)
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/layout/guess.py", line 104, in guess_layout_from_history
    for (revpaths, revnum, revprops) in changed_paths:
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/logwalker.py", line 60, in iter_all_changes
    revpaths = get_revision_paths(revnum)
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/logwalker.py", line 295, in get_revision_paths
    return self.cache.get_revision_paths(revnum)
  File "/usr/lib/python2.7/dist-packages/bzrlib/plugins/svn/cache/tdbcache.py", line 187, in get_revision_paths
    raise KeyError("missing revision paths for %d" % revnum)
KeyError: 'missing revision paths for 78'

You can report this problem to Bazaar's developers by running
    apport-bug /var/crash/bzr.1000.2012-02-19T22:12.crash
if a bug-reporting window does not automatically appear.

</pre>
]]></content:encoded><dc:date>2012-02-19T22:13:44Z</dc:date></item><item><title>N9 Hackathon in Vienna</title><guid isPermaLink="false">n9-hackathon</guid><link>http://mg.pov.lt/blog/n9-hackathon</link><description>Last weekend I attended the N9 Hackathon in Vienna. Nokia kindly sponsored all food and accommodation costs and, at the ...</description><content:encoded><![CDATA[
<p>Last weekend I attended the <a
href="https://metalab.at/wiki/N9_Hackathon">N9 Hackathon</a> in Vienna.
Nokia kindly sponsored all food and accommodation costs and, at the very end,
surprised me with an entirely unexpected gift of a N9 phone.</p>

<p>Vienna: great transportation system, delicious food (either that, or I was
always extremely hungry when I ate), huge portions, restaurants open until
midnight.  Shame I didn't have time to see the city itself.</p>

<p>The <a href="http://swipe.nokia.com/">N9</a> is a gorgeous phone; much more
so in real life than in pictures.</p>

<p>After some hassle upgrading the <a href="http://qt.nokia.com/downloads/">Qt
SDK</a> (the provided upgrade tool managed to somehow remove the Qt Creator
IDE while purporting to upgrade it; I had to reinstall the entire SDK) and
flashing <a href="http://mg.pov.lt/blog/n950">my N950</a> with Beta 2 firmware
(Qt SDK 1.1.3 produces apps incompatible with the old Beta 1 firmware of the
N950) I started prototyping a time tracking application in <a
href="http://doc.trolltech.com/4.7-snapshot/qml-intro.html">QML</a>,
while learning QML and the <a
href="https://www.developer.nokia.com/swipe/ux/pages/building_blocks.html">N9
design guidelines</a> at the same time.</p>

<p>Converting the pretty pictures into QML was harder than I expected, but at
the end of the second day I had <a
href="http://github.com/mgedmin/qml-time-tracker">something</a> that looked
like a native N9 application.</p>

<div style="text-align: center; float: none; margin-left: 1em">
<a href="http://mg.pov.lt/qml-timetracker-prototype.png"
   title="click for full size image"
><img src="http://mg.pov.lt/qml-timetracker-prototype.png"
       alt="screenshot of the prototype" width="306" height="306" /></a>
</div>

<p>Most useful reference pages were:</p>
<ul>
  <li>The <a href="http://doc.trolltech.com/4.7-snapshot/qdeclarativeintroduction.html">QML
    syntax introduction</a> (which felt incomplete, but was almost adequate in the end).</li>
  <li>The <a href="http://harmattan-dev.nokia.com/docs/library/html/qt-components/qt-components-meego-componentlist.html?tab=1">list of Harmattan-specific QML components</a>.</li>
  <li>The <a href="http://harmattan-dev.nokia.com/docs/library/html/qt4/qdeclarativeelements.html?tab=1">list of standard QML components</a>.</li>
  <li>The <a
    href="https://www.developer.nokia.com/swipe/ux/pages/building_blocks.html">UI
    building blocks pages</a> mentioned above (pretty pictures! pretty colours! I <em>like</em> shiny things!).</li>
  <li><a href="http://harmattan-dev.nokia.com/docs/library/html/qt-components/qt-components-meego-interfaceguide.html">Harmattan
    Qt Components User Interface Guidelines</a>: pixel and font sizes of the
    standard UI elements.  (Ignore the "import UIConstants.js" red herrings;
    it's an internal thing <em>apparently</em>, and you can't use it
    directly from your 3rd-party apps.  Unless you find and copy UIConstants.js
    into your project, after figuring out if the licence allows it, which
    seemed too much of a hassle for me to even start.  So I hardcoded all
    the numbers directly, like a bad programmer who doesn't know about
    constants.)</li>
  <li>The <a
      href="http://harmattan-dev.nokia.com/docs/library/html/qt-components/qt-components-meego-simpletutorial.html?tab=1">TutorialApplication</a>
      sample, finding the sources of which was unexpectedly <em>difficult</em>
      -- a straight git clone of the <a
      href="http://qt.gitorious.org/qt-components/qt-components">qt-components
      repository</a> gives you something too recent to run with the older
      qt-components version on the N9.  I ended up using apt-get source
      qt-components in Scratchbox to download the <a
      href="http://harmattan-dev.nokia.com/pool/harmattan-beta/free/q/qt-components/qt-components_1.0~git20110525-1+0m6.tar.gz">source
      tarball</a> of version 1.0~git20110525-1+0m6.  Look in
      qt-components/examples/meego/.</li>
</ul>

<p>Finally, workflow.  QML is parsed at run time (application startup time,
unless you delay the loading), which means no recompilation ought to be
required to make changes, which means short development feedback cycles ought
to be possible.  So I was not happy about having to wait several seconds after
hitting Run in Qt Creator, while it built a .deb, copied it to the device over
wifi or USB networking, installed and ran it there.  Deploying to the Qt
Simulator is quicker, but not as much as I think it ought to be.  Plus, the
Qt Simulator apparently cannot simulate the landscape mode of the N9,
<em>and</em> it lies about the default font size of QML Text elements (if you
do not specify a pixelSize, text elements will look all right in the simulator,
but ridiculously tiny on the N9).</p>

<p>In the end I cobbled up a <a
href="https://github.com/mgedmin/qml-time-tracker/blob/master/quicktest">shell
script</a> that rsyncs my updated QML files to the device and runs a short
<a href="https://github.com/mgedmin/qml-time-tracker/blob/master/quicktest.py">Python
script</a> (over ssh) to launch them.  You need rsync and PySide installed on
the device for this, obviously, as well as having
SSH set up for passwordless logins.  As a bonus, I can now do QML development
during lunch, directly on the device, with <a
href="http://mg.pov.lt/770/dists/harmattan/user/binary-armel/vim_7.3.260-1_armel.deb">vim</a>,
enjoying proper <a
href="http://gitorious.org/qt-qml-demo-playground/qt-qml-demo-playground/blobs/raw/master/qml.vim">syntax
highlighting</a>.
:)</p>

<p>Oh, and my code is <a href="http://github.com/mgedmin/qml-time-tracker/">up
on Github</a>.</p>
]]></content:encoded><dc:date>2011-10-12T11:25:13Z</dc:date></item><item><title>Porting FBReader to Meego 1.2 Harmattan</title><guid isPermaLink="false">harmattan-fbreader</guid><link>http://mg.pov.lt/blog/harmattan-fbreader</link><description>Andrew Olmsted built the first FBReader packages for Harmattan, after tweaking the build system a bit. The desktop version of ...</description><content:encoded><![CDATA[
<p><a href="http://wiki.meego.com/User:Fiferboy">Andrew Olmsted</a> built
the first <a href="http://www.fbreader.org">FBReader</a> packages for Harmattan, after <a
href="http://mg.pov.lt/fiferboy-fbreader-harmattan.patch">tweaking the build
system</a> a bit.  The desktop version of FBReader already used Qt 4, and
ran almost unmodified, but with some bugs (<a
href="http://pastie.org/2238548">segfault</a> on task switch) and ugly
UI.</p>

<p>I started with the Ubuntu packages for FBReader, since they used a more
sane build system for .debs (compared to upstream's <a
href="https://github.com/geometer/FBReader/blob/master/build_packages.sh">funky
shell script</a>).  Some tweaks were needed to make it build in Scratchbox:
since GTK+ and Hildon libraries aren't available on Harmattan, I had to disable
the building of -gtk and -maemo versions of libzlui.  I also got to learn
a new tool &mdash; <a href="http://wiki.debian.org/UsingQuilt">quilt</a>.</p>

<p>Fixing the <a href="http://pastie.org/2281152">segfault</a> took a couple of days of <a
href="http://pastie.org/2281247">debugging</a>, studying the <a
href="https://github.com/geometer/FBReader/blob/master/zlibrary/ui/src/qt4/view/ZLQtViewWidget.cpp#L97">source</a>
<a href="https://github.com/geometer/FBReader/blob/master/zlibrary/ui/src/qt4/view/ZLQtPaintContext.cpp#L47">code</a>
of both FBReader and Qt itself, and asking for help on IRC.  Turns out
FBReader was holding an active QPainter instance for too long, and its backing
pixmap got destroyed (or, rather, converted from an OpenGL texture to a plain
X11 pixmap) during a task switch, causing the crash.  I'm probably describing
this wrong BTW, but, in any case, adding QPainter::begin() and QPainter::end()
calls in the paintEvent handler fixed the segfault.</p>

<p>Next, a small tweak in the .desktop file to make FBReader a single-instance
application: change <tt>Exec=FBReader</tt> to <tt>Exec=single-instance
  /usr/bin/FBReader</tt> (I'm paraphrasing slightly).</p>

<p>Then, a more ambitious goal: making FBReader intercept volume keys and
use them for scrolling.  Google gave me a <a
href="http://www.developer.nokia.com/Community/Discussion/showthread.php?226283-Grab-Volume-Keys-on-Harmattan">pointer</a>
to <a href="http://apidocs.meego.com/1.2/qmsystem/classMeeGo_1_1QmKeys.html">QmKeys</a>, which was the
wrong API to use here, but gave me a lead to qmkeyd2, which appears to be an
<a href="https://www.meego.gitorious.org/meego-middleware/qmsystem/blobs/5edeec3815de0a2cb81e305e504f072460efe30d/keyd/qmkeyd.cpp">open
source</a> daemon, which gave me a lead to sysuid, another <a
href="https://meego.gitorious.org/meegotouch/meegotouch-systemui/blobs/master/src/extensions/volume/volumebarlogic.cpp">open
source</a> daemon, which in turn gave me a lead to <a
href="http://harmattan-dev.nokia.com/unstable/beta/api_refs/showdoc.php?pkn=libresourceqt&wb=daily-docs&url=Li94bWwvZGFpbHktZG9jcy9saWJyZXNvdXJjZXF0">libresourceqt</a>,
and that was the right API at last.</p>

<p>Volume keys generate regular key events for XF86AudioRaiseVolume and
XF86AudioLowerVolume, but they're also intercepted by qmkeyd2, which tells
all subscribers (and sysuid is one) about them.  Which subscriber gets to
react is determined by the resource policy framework.  So what I needed to do
in FBReader was acquire the ScaleButtonResource when FBReader starts up or gets
focus, and release it when FBReader quits or goes into background.  That
also required some IRC help until I discovered <a
href="http://doc.qt.nokia.com/latest/qobject.html#installEventFilter">installEventFilter()</a>
and the <a
href="http://doc.qt.nokia.com/latest/qevent.html#Type-enum">ApplicationActivate/ApplicationDeactivate</a>
events. And <a
href="http://doc.qt.nokia.com/latest/qcoreapplication.html#instance">QApplication::instance()</a>.</p>

<p>The various tools available in the developer firmware were invaluable:
openssh, gdb, valgrind, strace, xev, xprop, lsof, netstat.  Also, I would not
have achieved my second goal without being able to look at the sources of Meego
system components (qmkeyd, sysuid).  Yay open source!</p>

<p>Here are <a
href="http://mg.pov.lt/fbreader-0.12.10dfsg-1ubuntu2mg9-harmattan.patch">my
changes to the source code</a>.  You can find my modified <a href="http://mg.pov.lt/770/dists/harmattan/experimental/source/fbreader_0.12.10dfsg-1ubuntu2mg9.diff.gz">Debian packaging
files</a>, as well as <strong>prebuilt binary packages</strong> (with full
debug info, for gdb goodness), in my <a
href="http://mg.pov.lt/770/">experimental harmattan apt repository</a>.
The UI is still ugly and non-native, but it doesn't matter much in fullscreen
mode <tt>:)</tt> .</p>

<p>Note to self: when next building fbreader, make sure the 2 megabyte
<tt>tags</tt> file doesn't end up in the <tt>.diff.gz</tt>.  And speaking of crud
in source packages, the <a
href="http://mg.pov.lt/770/dists/harmattan/user/binary-armel/vim_7.3.260-1_armel.deb">vim
package</a> I built for Harmattan the other day contains the entire 50 meg
<tt>.hg</tt> in the <tt>.orig.tar.gz</tt>.  I need to figure out how to
tell dh_make to omit it.</p>

]]></content:encoded><dc:date>2011-07-29T00:16:38Z</dc:date></item><item><title>Nokia N950</title><guid isPermaLink="false">n950</guid><link>http://mg.pov.lt/blog/n950</link><description>Last Thursday I received a package containing something called the Nokia N950 development kit. Sweet sweet hardware, shame it's not ...</description><content:encoded><![CDATA[
<p>Last Thursday I received a package containing something called the <a
href="http://wiki.maemo.org/N950">Nokia N950</a> development kit.  Sweet sweet
hardware, shame it's not going to be sold to end users.  The software is
visibly an unfinished pre-release version, but shows great potential.  There
are almost no 3rd-party apps, which is why Nokia is loaning these N950s to
random developers.</p>

<p>I intend to port <a href="http://mg.pov.lt/gtimelog/">GTimeLog</a> to it.
Although my more immediate need is to have <a
href="http://www.fbreader.org/">FBReader</a>, so that I can stop carrying both
this one and my N900 with me everywhere.  Also, <a
href="http://www.vim.org/">vim</a> would be nice.</p>

<p>I've already <a href="http://pastie.org/2218247">hacked up</a> Lithuanian
support to the virtual and hardware keyboards, thanks to the very nice design
of <a href="http://wiki.meego.com/Maliit">Maliit</a>.  As a comparison, I've
had my N900 for a year and a half, and I still can't type Lithuanian on it.
XKB is not fun.</p>
]]></content:encoded><dc:date>2011-07-19T12:44:52Z</dc:date></item><item><title>Stuff I've been doing recently</title><guid isPermaLink="false">december-updates-2010</guid><link>http://mg.pov.lt/blog/december-updates-2010</link><description>objgraph got shiny documentation built with Sphinx . Python 3.x support, thanks to Stefano Rivera. Python 2.4 and 2.5 support, ...</description><content:encoded><![CDATA[
<p><a href="http://pypi.python.org/pypi/objgraph">objgraph</a> got</p>
<ul>
  <li><a href="http://mg.pov.lt/objgraph/">shiny documentation</a> built with
      <a href="http://sphinx.pocoo.org/">Sphinx</a>.</li>
  <li>Python 3.x support, thanks to Stefano Rivera.</li>
  <li>Python 2.4 and 2.5 support, thanks to Daniel Blackburn.</li>
  <li><a href="http://mg.pov.lt/objgraph/CHANGES.html">assorted smaller improvements</a>.</li>
</ul>

<p><a href="http://pypi.python.org/pypi/zodbbrowser">zodbbrowser</a> got</p>
<ul>
  <li>support for all ZODB databases, not just those with a
      Zope 3/Bluebream-style root folder/local site.</li>
  <li>the ability to cope better with broken objects (due to the way ZODB
      works, not having some of your modules on the Python pack can break
      unpickling; zodbbrowser now handles this kind of situation better).</li>
  <li><a href="http://pypi.python.org/pypi/zodbbrowser#changes">assorted smaller improvements</a>.</li>
  <li>a slow but inevitable shift of focus from "use it as a plugin for your
      Zope 3 app" to "it's a standalone tool for inspecting ZODB contents".
      (Both use cases are still supported, and will be for the foreseeable
      future.)</li>
</ul>

<p><a href="http://pypi.python.org/pypi/imgdiff">imgdiff</a> got</p>
<ul>
  <li>its first public release.</li>
  <li>some experimental code to actually find and highlight the differing
      parts of the images:
      <br />
      <br />
      <center>
        <img src="http://mg.pov.lt/imgdiff-highlighting.png"
             width="739" height="403"
             alt="example of highlighted image differences" />
      </center>
      <br />
      This works better when both images are the same size, although there's
      experimental (and somewhat buggy) code to try all possible alignments.
      I could use some help here; image processing is not something I'm
      familiar with, and <a href="http://stackoverflow.com/search?q=difference+between+images">searching
        StackOverflow</a> didn't help beyond reminding me of the existence
      of PIL's ImageChops.difference(), which is for same-sized images only.
      Many of the results there are about comparing photos, where things like
      lighting levels matter.  What I need is a diff for computer-generated
      images, where some things may be shifted around a bit, by different
      amounts, but are essentially the same.  Are there any two-dimensional
      sequence diff algorithms?
    </li>
  </ul>

]]></content:encoded><dc:date>2010-12-19T00:35:40Z</dc:date></item><item><title>Profiling with Dozer</title><guid isPermaLink="false">profiling-with-dozer</guid><link>http://mg.pov.lt/blog/profiling-with-dozer</link><description>Dozer is mostly known for its memory profiling capabilities, but the as-yet unreleased version has more. I've talked about log ...</description><content:encoded><![CDATA[
<p><a href="http://pypi.python.org/pypi/Dozer">Dozer</a> is mostly known for
its memory profiling capabilities, but the as-yet unreleased version has
more.  I've <a href="http://mg.pov.lt/blog/capturing-logs-with-dozer.html">talked
  about log capturing</a>, now it's time for</p>

<p><strong>Profiling</strong></p>

<p>This WSGI middleware profiles every request with the cProfile module.
To see the profiles, visit a hidden URL <tt>/_profiler/showall</tt>:</p>

<p><a href="http://mg.pov.lt/dozer-all-profiles.png"><img src="http://mg.pov.lt/dozer-all-profiles-small.png" alt="List of profiles" /></a></p>

<p>What you see here is heavily tweaked in <a
  href="http://bitbucket.org/mgedmin/dozer">my <del>fork</del> <ins>branch</ins>
  of Dozer</a>; upstream version had no Cost column and
didn't vary the background of Time by age (that
last bit helps me see clumps of requests).</p>

<p>Here's what an individual profile looks like:</p>

<p><a href="http://mg.pov.lt/dozer-one-profile.png"><img src="http://mg.pov.lt/dozer-one-profile-small.png" alt="One profile" /></a></p>

<p>The call tree nodes can be expanded and collapsed by clicking on the
function name.  There's a hardcoded limit of 20 nesting levels (upstream had a
limit of 15), sadly that appears not to be enough for practical purposes,
especially if you start profiling Zope 3 applications...</p>

<p>You can also take a look at the WSGI environment:</p>

<p><a href="http://mg.pov.lt/dozer-environ.png"><img src="http://mg.pov.lt/dozer-environ-small.png" alt="WSGI environment expanded" /></a></p>

<p>Sadly, nothing about the response is captured by Dozer.  I'd've liked to
show the Content-Type and perhaps Content-Length in the profile list.</p>

<p>The incantation in <tt>development.ini</tt> is</p>
<blockquote><pre>
[filter-app:profile]
use = egg:Dozer#profile
profile_path = /tmp/profiles
next = main
</pre></blockquote>
<p>Create an empty directory /tmp/profiles and <strong>make sure other users
  cannot write to it</strong>.  Dozer stores captured profiles as Python
pickles, which are <a
  href="http://docs.python.org/library/pickle.html#relationship-to-other-python-modules">insecure</a>
and allow <strong>arbitrary
  command execution</strong>.</p>

<p>To enable the profiler, run paster like this:</p>
<blockquote><pre>
<span class="prompt">$</span> <span class="typing">paster serve development.ini -n profile</span>
</pre></blockquote>

<p><strong>Bonus feature: call graphs</strong></p>

<p>Dozer also writes a call graph in Graphviz "dot" format in the profile
directory.  Here's the graph corresponding to the profile you saw earlier,
as displayed by the excellent <a
  href="http://code.google.com/p/jrfonseca/wiki/XDot">XDot</a>:</p>

<p><a href="http://mg.pov.lt/dozer-graph.png"><img src="http://mg.pov.lt/dozer-graph-small.png" alt="Call graph" /></a></p>

<p>See the fork where the "hot" red path splits into two?</p>

<p><a href="http://mg.pov.lt/dozer-graph-zoom1.png"><img src="http://mg.pov.lt/dozer-graph-zoom1-small.png" alt="Call graph, zoomed in" /></a></p>

<p>On the left we have Routes deciding to spend 120 ms (70% total time)
recompiling its route maps.  On the right we have the actual request dispatch.
The actual controller action is called a bit further down:</p>

<p><a href="http://mg.pov.lt/dozer-graph-zoom2.png"><img src="http://mg.pov.lt/dozer-graph-zoom2-small.png" alt="Call graph, zoomed in" /></a></p>

<p>Here it is, highlighted.  42 ms (24% total time), almost all of which is
spent in SQLAlchemy, loading the model object (a 2515 byte image stored as a
blob) from SQLite.</p>

<p><strong>A mystery: pickle errors</strong></p>

<p>When I first tried to play with the Dozer profiler, I was attacked by
innumerable exceptions.  Some of those were due to a lack of configuration
(profile_path) or invalid configuration (directory not existing), or not
knowing the right URL (going to <tt>/_profiler</tt> raised TypeError).  I
tried to make Dozer's profiler more forgiving or at least produce clearer
error messages in <a href="http://bitbucket.org/mgedmin/dozer">my <del>fork</del> <ins>branch</ins></a>,
e.g. going to <tt>/_profiler</tt> now displays the profile list.</p>

<p>However some errors were very mysterious: some pickles, written by Dozer
itself, could not be unpickled.  I added a try/except that put those at the end
of the list, so you can see and delete them.</p>

<p><a href="http://mg.pov.lt/dozer-errors.png"><img src="http://mg.pov.lt/dozer-errors-small.png" alt="Pickle errors" /></a></p>

<p>Does anybody have any clues as to why <a
  href="http://bitbucket.org/mgedmin/dozer/src/6b6884445863/dozer/profile.py#cl-183">profile.py</a>
might be writing out broken pickles?</p>

<p><strong>Update</strong>: as Ben says in the comments, my changes have
been accepted upstream.  Yay!</p>
]]></content:encoded><dc:date>2010-08-07T03:06:33Z</dc:date></item><item><title>Capturing logs with Dozer</title><guid isPermaLink="false">capturing-logs-with-dozer</guid><link>http://mg.pov.lt/blog/capturing-logs-with-dozer</link><description>Dozer is mostly known for its memory profiling capabilities, but the as-yet unreleased version has more: Log capturing This WSGI ...</description><content:encoded><![CDATA[
<p><a href="http://pypi.python.org/pypi/Dozer">Dozer</a> is mostly known for
its memory profiling capabilities, but the as-yet unreleased version has
more:</p>

<p><strong>Log capturing</strong></p>

<p>This WSGI middleware intercepts logging calls for every request.  Here
we see a toy <a href="http://pylonshq.com/">Pylons</a> application I've
been working on in my spare time. Dozer added an info bar at the top:</p>

<p><a href="http://mg.pov.lt/dozer-logview-prompt.png"><img src="http://mg.pov.lt/dozer-logview-prompt-small.png" alt="Dozer's infobar" /></a></p>

<p>When you click on it, you get to see all the log messages produced for this
request.  I've set SQLAlchemy's loglevel to INFO in my
<tt>development.ini</tt>, which produces:</p>

<p><a href="http://mg.pov.lt/dozer-logview.png"><img src="http://mg.pov.lt/dozer-logview-small.png" alt="Dozer's log viewer" /></a></p>

<p>(Why on Earth does SQLAlchemy think I want to see the memory address of the
Engine object in my log files, I don't know.  The parentheses contain
argument values for parametrized queries, of which there are none on this
page.)</p>

<p>Upstream version displays absolute timestamps (of the YYYY-MM-DD
HH:MM:SS.ssssss variety) in the first column; <a
  href="http://bitbucket.org/mgedmin/dozer">my fork</a> shows deltas in
milliseconds.  The incantation in <tt>development.ini</tt> is</p>
<blockquote><pre>
[filter-app:logview]
use = egg:Dozer#logview
next = main
</pre></blockquote>
<p>which makes it disabled by default.  To enable, you run paster like this:</p>
<blockquote><pre>
<span class="prompt">$</span> <span class="typing">paster serve development.ini -n logview</span>
</pre></blockquote>

<p>(Upstream version lacks the paste entry point for logview; it's in my
fork, for which I submitted a pull request weeks ago like a good open-source
citizen.  Incidentally, patches for stuff I maintain have been known to
languish for <em>years</em> in <em>my</em> inbox, so I'm not one to throw
stones.)</p>

<p>Next: <a href="http://mg.pov.lt/blog/profiling-with-dozer.html">profiling with Dozer</a>.</p>

<p><strong>Update:</strong> Tom Longson <a
  href="http://truefalsemaybe.com/2008/11/profiling-sql-in-pylons-with-dozer/">blogged
  about this</a> back in 2008!  And his CSS is prettier.</p>
]]></content:encoded><dc:date>2010-08-07T02:27:59Z</dc:date></item><item><title>irclog2html is now on PyPI</title><guid isPermaLink="false">irclog2html-on-pypi</guid><link>http://mg.pov.lt/blog/irclog2html-on-pypi</link><description>irclog2html , the IRC log to HTML converter, is now (finally!) available from the Python Package Index . In other ...</description><content:encoded><![CDATA[
<p><a href="http://mg.pov.lt/irclog2html/">irclog2html</a>, the IRC log to HTML
converter, is now (finally!) available from the <a
  href="http://pypi.python.org/pypi/irclog2html">Python Package Index</a>.</p>

<p>In other news, logs2html now copies <a
  href="http://mg.pov.lt/maemo-irclog/irclog.css">irclog.css</a> to the
destination directory (if it doesn't exist there already).  I've been noticing
logs produced with irclog2html on random places, and sometimes they were
unstyled; hopefully this will become rare now.</p>
]]></content:encoded><dc:date>2010-08-06T01:32:57Z</dc:date></item><item><title>Serving ePub files to Android 1.6</title><guid isPermaLink="false">android-fbreader-epub-problem</guid><link>http://mg.pov.lt/blog/android-fbreader-epub-problem</link><description>If you click on an ePub download link in the Android browser and get back an error saying &quot;Cannot download. ...</description><content:encoded><![CDATA[
<p> If you click on an ePub download link in the Android browser and get back
an error saying "Cannot download. The content type is not supported on the
device", then make sure the web server is setting the Content-Type header to
"application/epub+zip" and not "application/octet-stream".  When you do this,
Android will launch FBReaderJ automatically (provided that you have it
installed, of course).</p>
]]></content:encoded><dc:date>2010-07-22T23:23:38Z</dc:date></item><item><title>N900 connection sharing the hard way</title><guid isPermaLink="false">n900-connection-sharing</guid><link>http://mg.pov.lt/blog/n900-connection-sharing</link><description>My N900 has a SIM card with a flat-rate 3G data plan. My laptop hasn't. What do I do when ...</description><content:encoded><![CDATA[
<p>My N900 has a SIM card with a flat-rate 3G data plan.  My laptop hasn't.
What do I do when I want to use the Internet on my laptop somewhere that
doesn't have WiFi?  Well, there are many options:</p>

<p><strong>Option 1: N900 as a USB modem</strong></p>

<p>Use the provided USB cable to connect the N900 to the laptop.  Choose "PC
Suite" mode on the N900 when you get the USB connection menu.  The laptop
now sees your N900 as a bog-standard USB 3G modem.  Use Network Manager to
connect to the internet.</p>

<blockquote>
  <p>Pros: no extra setup required.  The N900 and the laptop can both access
  the Internet at the same time.</p>

  <p>Cons: you have to use a USB cable (I hate cables).  You cannot ssh into your
  N900 (and ssh is my primary file transfer protocol between the laptop and the
  M900).</p>
</blockquote>

<p><strong>Option 2: N900 as a Bluetooth DUN modem</strong></p>

<p>Install <a
  href="http://maemo.org/downloads/product/Maemo5/bluetooth-dun/">Bluetooth DUN
  support</a> from Maemo Extras.  Then use it like you would any other phone
that has Bluetooth DUN.</p>

<blockquote>
  <p>Pros: no cables.</p>

  <p>Cons: Bluetooth is the worst technology <em>ever</em>.  I never had it
  work reliably.  Plus, Network Manager in Ubuntu 10.04 doesn't support
  Bluetooth DUN (it supports only Bluetooth PAN, as far as I know).</p>
</blockquote>

<p><strong>Option 3: N900 as a WiFi access point with Joikuspot</strong></p>

<p>I haven't tried this.</p>

<blockquote>
  <p>Pros: simple (hopefully), no cables required.</p>

  <p>Cons: Joikuspot is non-free.  I'm not an absolute zealot, but I will
  avoid closed-source stuff when open-source alternatives are available.</p>
</blockquote>

<p><strong>Option 4: N900 as a WiFi access point with <a href="http://mobilehotspot.garage.maemo.org/">Mobilehotspot</a></strong></p>

<p>I haven't tried this either.</p>

<blockquote>
  <p>Pros: it's an open-source app available from Maemo Extras.  No cables
  required.</p>

  <p>Cons: requires a non-standard kernel (or so I've heard).  Way outside my
  comfort level.</p>
</blockquote>

<p><strong>Option 5: N900 as a WiFi access point with shell scripts</strong></p>

<p>Here's the shell script I run on my N900: <a
href="http://mg.pov.lt/share-wifi">share-wifi</a>.  It sets up an ad-hoc WiFi
network, and starts a DHCP and DNS server (dnsmasq).  Sadly, it cannot set up
connection sharing (NAT), so I rely on OpenSSH as a SOCKS5 proxy.  The whole
setup is like this:</p>

<ol>
  <li>You want the latest firmware (PR 1.2) to avoid <a
      href="https://bugs.maemo.org/show_bug.cgi?id=5712">this bug</a>.</li>
  <li>You need to have <a
      href="http://maemo.org/downloads/product/Maemo5/openssh/">OpenSSH</a> installed on
      the N900.  Also, setting up key-based authentication makes it more
      convenient.</li>
  <li>The script assumes that you've set up sudo on the N900 so that you
      can run any command as root.</li>
  <li>You need to have <a
      href="http://maemo.org/packages/view/wireless-tools/">wireless-tools</a>
      installed.  It's in the main SSU repository so you should be able to sudo
      apt-get install it (if it's not preinstalled; I don't remember).</li>
  <li>On the N900 run <tt>share-wifi</tt> in a terminal (optionally passing a
      WiFi channel number from 1 to 11, in case you need to avoid interference with
      nearby networks).</li>
  <li>On the laptop connect to the new n900 WLAN and run
      <tt>ssh -D 1080 user@n900</tt>.  You will get a shell session; the SOCKS proxy
      will be active while it is open.</li>
  <li>Reconfigure your laptop to use a SOCKS5 proxy on localhost:1080.
      For GNOME systems I've a couple of shell scripts: <a
      href="http://mg.pov.lt/proxy-on">proxy-on</a> and <a
      href="http://mg.pov.lt/proxy-off">proxy-off</a>.  For applications
      that do not use the GNOME proxy settings (such as Subversion access
      over SSH), use <a href="http://mg.pov.lt/blog/escaping-hotel-firewall.html#tsocks">tsocks</a>.</li>
  <li>When done, hit Ctrl-C on the N900 to terminate the sharing script.</li>
</ol>

<blockquote>
  <p>Pros: no non-free software or custom kernel required.  No cables.</p>

  <p>Cons: complicated to set up.  No WLAN power savings available for ad-hoc
     networks, so battery life is extremely poor (~2 hours).  But, hey, <i>no
     cables!</i></p>
</blockquote>
]]></content:encoded><dc:date>2010-07-20T22:41:13Z</dc:date></item><item><title>ImportError: No module named _md5</title><guid isPermaLink="false">no-module-md5</guid><link>http://mg.pov.lt/blog/no-module-md5</link><description>If you're using virtualenv, and after a system upgrade you get errors like ... File &quot;...&quot;, line ... from hashlib ...</description><content:encoded><![CDATA[
<p>If you're using virtualenv, and after a system upgrade you get errors like</p>
<blockquote><pre>
...
  File "...", line ...
    from hashlib import md5
  File "/usr/lib/python2.6/hashlib.py", line 63, in __get_builtin_constructor
     import _md5
ImportError: No module named _md5
</pre></blockquote>

<p>this means that the copy of the python executable in your virtualenv/bin
directory is outdated and you should update it:</p>

<blockquote><pre>
<span class="prompt">$</span> cp /usr/bin/python2.6 /path/to/venv/bin/python
</pre></blockquote>

<p>or, better yet, recreate the virtualenv.</p>
]]></content:encoded><dc:date>2010-07-06T22:20:04Z</dc:date></item><item><title>Booting ISO images from a USB drive</title><guid isPermaLink="false">booting-iso-from-usb</guid><link>http://mg.pov.lt/blog/booting-iso-from-usb</link><description>Dear lazyweb, I would like to download an arbitrary ISO image (say, a Ubuntu 10.04 Desktop CD ) into a ...</description><content:encoded><![CDATA[
<p>Dear lazyweb, I would like to download an arbitrary ISO image (say, a <a
  href="http://www.ubuntu.com/getubuntu/download">Ubuntu 10.04 Desktop CD</a>)
into a directory of a USB flash drive, and then make that USB flash drive boot
that ISO image.  I do <em>not</em> want to</p>

<ul>
  <li>re-partition or re-format the flash drive (this eliminates <a
    href="https://launchpad.net/usb-creator">usb-creator</a>, AFAIU)</li>
  <li>extract the contents of the ISO image into the root of the USB drive
      (this eliminates <a
       href="http://sourceforge.net/apps/trac/unetbootin/wiki/howitworks">unetbootin</a>)</li>
  <li>skip the ISO's bootloader and directly boot the kernel+initramfs from
      the ISO (eliminates <a
       href="https://lists.ubuntu.com/archives/ubuntu-users/2010-April/216901.html">this
       recipe</a>, and <a
       href="https://lists.ubuntu.com/archives/ubuntu-users/2010-April/216426.html">this</a><a
       href="https://lists.ubuntu.com/archives/ubuntu-users/2010-April/216429.html">
       recipe</a>)</li>
</ul>

<p>I just want a bootloader on the USB to read the VFAT filesystem, mount the ISO image as a loop device,
then chain-load the bootloader from that ISO.  Bonus points for having a menu letting me choose one of
several ISO images.  Running a script to edit a text file (say, grub's config)
to get that menu is fine.</p>

<p>Is this even possible?  If not, can I at least have two out of three (no
partition/extraction, but skipping intrinsic bootloader is fine)?</p>

<p><strong>Solution that I finally chose (from <a href="http://ubuntuforums.org/showthread.php?t=1288604">Ubuntu forums</a>):</strong></p>

<p>Plug in USB key.  Find out the mount point (<tt>/media/<em>disk</em></tt>) and the device
name (<tt>/dev/sd<em>x</em></tt>) of the USB key with</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">mount|grep /media</span>
</pre></blockquote>

<p>Install GRUB 2 into the USB key with</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">sudo grub-install --root-directory=/media/<em>usbdisk</em> /dev/sd<em>x</em></span>
</pre></blockquote>

<p>If it says something about embedding being impossible and falling back
to UNRELIABLE blocklist-based setup, run</p>

<blockquote><pre>
<span class="prompt">$</span> <span class="typing">sudo grub-install --root-directory=/media/<em>usbdisk</em> /dev/sd<em>x</em> --force</span>
</pre></blockquote>

<p><a href="http://www.ubuntu.com/desktop/get-ubuntu/download">Download a CD
image</a>, let's say <tt>ubuntu-10.10-desktop-i386.iso</tt>.  Put it into
<tt>/media/<em>usbdisk</em>/ubuntu/</tt>.  </p>

<!--
<span class="prompt">$</span> <span class="typing">mkdir /media/<em>usbdisk</em>/ubuntu</span>
<span class="prompt">$</span> <span class="typing">cd /media/<em>usbdisk</em>/ubuntu</span>
<span class="prompt">$</span> <span class="typing">wget http://ftp.litnet.lt/pub/ubuntu-cd/maverick/ubuntu-10.10-desktop-i386.iso</span>
-->

<p>Create a text file <tt>/media/<em>usbdisk</em>/boot/grub/grub.cfg</tt> with</p>

<blockquote><pre>
menuentry "<em>Ubuntu 10.10 (x86 desktop livecd)</em>" {
    set isofile="<em>/ubuntu/ubuntu-10.10-desktop-i386.iso</em>"
    loopback loop $isofile
    linux (loop)/casper/vmlinuz boot=casper iso-scan/filename=$isofile quiet splash noprompt --
    initrd (loop)/casper/initrd.lz
}
</pre></blockquote>

<p>You can have as many ISO images as you want, just make sure to add a
menuentry for each.  There's no need to run grub-install again after adding
or removing a .iso file.  Oh, and if you want to use an ISO file for a
different distribution, you'll have to figure out the correct linux and initrd
lines somehow.</p>

<p><strong>Update:</strong> Ubuntu 13.04 changed the name of the kernel -- use
<tt>(loop)/casper/vmlinuz.efi</tt> instead of <tt>(loop)/casper/vmlinuz</tt>.</p>

<p>I tested it with the following images</p>

<ul>
  <li>ubuntu-10.04-desktop-i386.iso</li>
  <li>ubuntu-10.04-server-i386.iso</li>
  <li>ubuntu-10.04-server-amd64.iso</li>
  <li>ubuntu-10.10-desktop-i386.iso</li>
  <li><strong>ubuntu-11.04-desktop-i386.iso</strong></li>
  <li><strong>ubuntu-11.10-desktop-i386.iso</strong></li>
  <li><strong>ubuntu-12.04-desktop-i386.iso</strong></li>
  <li><strong>ubuntu-12.04-desktop-amd64.iso</strong></li>
  <li><strong>ubuntu-12.04-server-i386.iso</strong></li>
  <li><strong>ubuntu-12.04-server-amd64.iso</strong></li>
  <li><strong>ubuntu-12.10-desktop-i386.iso</strong></li>
  <li><strong>ubuntu-12.10-desktop-amd64.iso</strong></li>
  <li><strong>ubuntu-13.04-desktop-amd64.iso</strong></li>
</ul>

<p>This solution skips the CD-ROM's boot menu. I haven't found a better
one.</p>
]]></content:encoded><dc:date>2010-05-03T17:24:31Z</dc:date></item><item><title>Re: Web frameworks considered useful.</title><guid isPermaLink="false">web-frameworks-useful</guid><link>http://mg.pov.lt/blog/web-frameworks-useful</link><description>Martijn Faassen defends web frameworks in a rather longish post (you can tell it's 5 AM in the morning and ...</description><content:encoded><![CDATA[
<p>Martijn Faassen <a
  href="http://faassen.n--tree.net/blog/view/weblog/2010/04/15/0">defends web
  frameworks</a> in a rather longish post (you can tell it's 5 AM in the
morning and I've <em>nearly</em> defeated the unread post queue in Google
Reader).  I'd like to propose a condensed
version.  Consider this slogan:

<blockquote>
  Simple things should be easy; complicated things should be possible.
</blockquote>

<p><em>Frameworks</em> make simple things easy.  <em>Good</em> frameworks
keep the complicated thing possible; poorly-designed frameworks make the
complicated thing more difficult than necessary; bad frameworks make even
simple things complicated.</p>

<p>Doing everything from scratch merely makes things possible, but rarely
easy.</p>
]]></content:encoded><dc:date>2010-04-18T01:57:07Z</dc:date></item><item><title>Review: Grok 1.0 Web Development</title><guid isPermaLink="false">grok-book-review</guid><link>http://mg.pov.lt/blog/grok-book-review</link><description>Disclaimer: I received a free review copy of this book. The book links are affiliate links; I get a small ...</description><content:encoded><![CDATA[
<p>Disclaimer: I received a free review copy of this book. The book links are
affiliate links; I get a small amount from any purchase you make through them.</p>

<p><a href="http://grok.zope.org/">Grok</a> is a Python web framework, built on
top of the Zope Toolkit, which is the core of what used to be called Zope 3 and
is now rebranded as BlueBream.  Confused yet?  Get used to it: the small
pluggable components are the heart and soul of ZTK, and the source of its
flexibility. It's not surprising that people take the same approach on a larger
scale: take Zope 3 apart into smaller packages and reassemble them into
different frameworks such as Grok, BlueBream or repoze.bfg.</p>

<p style="float: right; margin: 0 0 1em 1em">
<a href="http://www.packtpub.com/grok-1-0-web-development/book/mid/150310wgpfxa?utm_source=mg.pov.lt&utm_medium=affiliate&utm_content=blog&utm_campaign=mdb_002715">
  <img src="http://mg.pov.lt/grok-book.jpg"
       width="250" height="309" alt="Grok 1.0 Web Development by Carlos de la Guardia" />
</a>
</p>

<p>The <a
  href="http://www.packtpub.com/grok-1-0-web-development/book/mid/150310wgpfxa?utm_source=mg.pov.lt&utm_medium=affiliate&utm_content=blog&utm_campaign=mdb_002715">Grok
  book by Carlos de la Guardia</a> introduces the framework by demonstrating
how to create a small but realistic To-do list manager.  I like this technique,
and it works pretty well.  The author covers many topics:</p> 
<ul>
  <li> creation of a new project </li>
  <li> simple views with Zope Page Templates </li>
  <li> automatic form generation from schemas (with tweaks) </li>
  <li> catalogs and indexes (my favourite chapter) </li>
  <li> security: users, roles, permissions; authentication and authorization </li>
  <li> extremely pluggable page layouts with viewlets and pagelets </li>
  <li> basic ZODB, blobs, ZEO, database packing, backups with repozo </li>
  <li> SQL databases, integration with SQLAlchemy (including a common
       transactional model) </li>
  <li> component architecture: adapters and utilities </li>
  <li> Martian: extending Grok by defining custom component directives </li>
  <li> very short intro to testing (zope.testing, unit tests and doctests,
       functional tests with zope.testbrowsing) and debugging (pdb; AJAXy
       debugger, which looks exactly like the Pylons one with an uglier skin) </li>
  <li> deployment (my second favourite chapter): paster, apache and mod_proxy,
       mod_wsgi, pound, squid, varnish, scalable deployments. </li>
</ul>
<p>Some important topics like internationalization, time zones, testing with
Selenium, and (especially) database migration (which is pretty specific for
ZODB) were not covered.</p>

<p> <strong>If you want to learn about Grok, this book will be useful</strong>,
but there's a caveat: there's the usual slew of typographical mistakes and
other errors I've come to expect from books published by Packt.  It's their
third book I've seen; all three had surprisingly high numbers of errors.  Some
had more, others had fewer.  The Grok book was on the high side and the first
one where I was tempted to record a "<a
  href="http://www.osnews.com/story/19266/WTFs_m">WTFs per page</a>" metric.
The mistakes are easy to notice and correct, so they didn't impede my
understanding of the book's content.  Disclaimer: I've been working with
Zope 3 for the last six-or-so years, so I was pretty familiar with the
underlying technologies, just not the thin Grok convenience layer. <strong>If
  minor errors annoy you, stay away.</strong>  I haven't noticed any major
factual errors, although there were what I would consider some pretty important
omissions:</p>

<ul>
  <li>ZODB is not as transparent as people tell you.  There are many gotchas,
  especially if you want to refactor your code without throwing away old
  databases.</li>
  <li><tt>bin/buildout</tt> is free to recursively remove anything under
  <tt>parts</tt>.  Keeping your database there is fine only if you don't mind
  occasionally starting from scratch.</li>
  <li><tt>repozo</tt> does not back up blobs.</li>
  <li>The ZODB transaction conflict resolution depends on being able to
  repeat requests several times; this is important if your code has external
  side effects (e.g. sends emails, creates files, pings 3rd party websites over
  HTTP).  Packages like megrok.rdb or zope.sendmail take care of this; it'd be
  nice to be shown how to do that  for your own code before you discover this
  issue the hard way when your app starts charging people's credit cards three
  times every now and then. </li>
  <li>You need to make sure you send out object events at appropriate times, or
  your catalog indexes won't be updated.</li>
  <li>Permission and role grants are persistent: if you delete a user and then
  create a new one with the same username, the new user will have all the roles
  and permissions granted to the old one.  If you implement user deletion, you
  need to explicitly remove old grants.</li>
  <li>The Zope security model expects every object to have a valid <code>__parent__</code>
  attribute; permission/role grants will not work properly on objects without a
  <code>__parent__</code>.  Most of the time this is taken care of
  automatically, but when it's not, you can get really confusing errors.</li>
  <li><code>applySkin</code> should only be used for browser requests; blindly
  calling it from a traversal event handler can break WebDAV/XML-RPC.
  (Incidentally, I should file a bug about that; it should abort if you pass a
  non-browser request instead of silently converting it into a browser
  request.)</li>
  <li>Allowing end-users to specify <code>++skin++</code> in the URL can be a
  security hole.</li>
</ul>

<p>Overall, Grok is pretty nice, especially compared to vanilla Zope 3.
However, when compared to frameworks like Pylons or Django, Grok appears more
complex and seemingly requires you to do additional work for unclear gain.  For
example, chapter 8 has you writing three components for every new form you add:
one for the form itself, one for a pagelet wrapping the form, and one for a
page containing the pagelet.  Most of that code is very similar with only the
names being different.  I'm sure there are situations where this kind of
extreme componentization pays off (e.g. it lets you override particular bits on
particular pages to satisfy a particular client's requests, without affecting
any other clients), but the book doesn't convincingly demonstrate those
advantages.  Again, I may be biased here since I've been enjoying those
advantages for the past six years, without ever having felt the pain of doing
similar customizations with a less flexible framework.  (It's a gap in my
professional experience that I'm itching to fill.) </p>

<p><strong>Update:</strong> <a
  href="http://lateral.netmanagers.com.ar/weblog/posts/BB885.html">some</a> <a
  href="http://blog.pythonisito.com/2010/03/review-grok-10-web-development.html">other</a>
<a
  href="http://www.jmcneil.net/2010/03/review-grok-1-0-web-development/">reviews</a>
on Planet Python.</p>

<p><strong>Update 2:</strong> <a
  href="http://blog.lowkster.com/2010/04/review-of-grok-web-development-10-part.html">Another
  review</a> (well, part 1 of one, but I got tired waiting for part 2).
</p>
]]></content:encoded><dc:date>2010-04-04T19:30:12Z</dc:date></item><item><title>Review: Python Testing: Beginner's Guide</title><guid isPermaLink="false">python-testing-review</guid><link>http://mg.pov.lt/blog/python-testing-review</link><description>I've been testing (as well as writing) Python code for the last eight years, so a book with the words ...</description><content:encoded><![CDATA[
<p>I've been testing (as well as writing) Python code for the last eight years,
so a book with the words <em>Begginer's Guide</em> prominently displayed on
the cover isn't something I'd've decided to buy for myself.  Nevertheless
I jumped at the offer of receiving a free e-copy for reviewing it.</p>

<p style="float: right; margin: 0 0 1em 1em">
<a href="http://www.packtpub.com/python-testing-beginners-guide/book/mid/1503107sqs3w?utm_source=mg.pov.lt&utm_medium=affiliate&utm_content=blog&utm_campaign=mdb_002714">
  <img src="http://mg.pov.lt/python-testing.jpg"
       width="250" height="309" alt="Python Testing: Beginner's Guide by Danien Arbuckle" />
</a>
</p>

<p><strong>Short summary:</strong> it's good book.  I learned a thing or two
from it.  I don't know well it would work as an introductionary text for
someone new to unit testing (or Python).  Some of the bits seemed
overcomplicated and underexplained, parts of the example code/tests seemed to
contain design decisions received from mysterious sources.</p>

<p>Incidentally, <a href="http://www.packtpub.com/">Packt</a> uses a simple yet
effective method for watermarking e-books: my name and street address are
displayed in the footer of every page.  What's funny is that the two non-ASCII
characters in the street name are replaced with question marks.  It's not a
data entry problem: the website that let me download those books shows my
address correctly, so it must be happening somewhere in the PDF production
process.  I didn't expect this kind of Unicode buggyness from a publisher.
Then again there were occasional strange little typographical errors in the
text, like not leaving a space in front of an opening parenthesis in an English
sentence, or using a never-seen-before <tt>+q=</tt> operator in Python code.  I
was also left wondering how the following sentence (page 225) could slip past
the editing process:</p>

<blockquote>
  doctest ignores everything between the <tt>Traceback (most recent last call)</tt>.
</blockquote>

<p>Thankfully those small mistakes did not detract from the overall message of
the book.</p>

<p>I liked the author's technique of showing subtly incorrect code, letting the
reader look at it and miss all the bugs, and then showing how unit or
integration tests catch the bugs the reader missed.  I'm pretty sure there's at
least one remaining bug that the author missed in the example package (storing
a schedule doesn't erase old data), which could serve for a new chapter on
regression testing if there's a second edition.</p>

<p>Summary of topics covered:</p>
<ul>
  <li>Terms: unit testing, integration testing, system testing.</li>
  <li>Basics of doctest and unittest, their strengths and weaknesses.</li>
  <li>Using mocks (with Mocker).</li>
  <li>Using Nose.</li>
  <li>Test-Driven Development with lots of example code.</li>
  <li>Using Twill.</li>
  <li>Integration testing with lots of example code.</li>
  <li>Using coverage</li>
  <li>Post-commit hooks to run tests with Bazaar, Mercurial, Git, Darcs,
      Subversion.</li>
  <li>Continuous integration with Buildbot</li>
</ul>

<p>I found the TDD cycle a bit larger than I generally like, but I believe it's
a matter of taste, and perhaps a shorter cycle wouldn't work as well in a
written medium.</p>

<p>I found it a bit jarring how the Twill chapter intrudes between the two
chapters showing unit testing and integration testing of the same sample
package.  I think it would've been better to swap the order of chapters 8 and
9.</p>

<p>I liked the technique presented for picking subsets of the code for
integration tests, although I wonder how well it would work on a larger
project.</p>

<p>Topics not covered:</p>
<ul>
  <li>Functional testing (which is very close but not exactly the same as
      system testing).</li>
  <li>Regression testing (page 46 contains advice about this without mentioning
      the term <em>regression testing</em>).</li>
  <li>Continuous integration with Hudson (simpler to set up than buildbot,
      easily covers 80% of cases).</li>
</ul>

<p>As you can see these holes are all rather small.</p>

<p>Probably the biggest weakness of the book is the complexity of some
things shown:
</p>

<ul>
  <li> writing mocks for pure unit tests </li>
  <li> mocking other instances of the same class under test </li>
  <li> even occasionally mocking <em>self</em>, which needs tricks like
       calling a method's <tt>im_func</tt> directly </li>
  <li> mocking <tt>__reduce_ex__</tt> so you can pickle mocks in an
       <em>integration test</em>, instead of using real classes or simple
       stubs. </li>
  <li> testing the same code multiple times: unit tests, several sets of
       integration tests that test ever-increasing subsets of classes </li>
  <li> <a href="http://buildbot.net/">Buildbot</a> instead of
       <a href="http://hudson-ci.org/">Hudson</a> </li>
</ul>

<p> Seeing the repetitive and redundant mock code in the first few doctest
examples I started asking <em>what's the point?</em>, but the book failed to
provide a compelling answer (the answer provided&mdash;it's easier to locate
bugs&mdash;works just as well for integration tests that focus on individual
classes).  And there are good answers for that question, like instant feedback
from your unit test suite.  Are they worth the additional development effort?
Maybe that depends on the developer.  I don't think they would help me, so I
tend to stick with low-level integration tests I call "unit tests" (as well as
system tests; it's always a mistake to keep all your tests in a single level).
I'm slightly worried that this book might give the wrong impression (testing is
hard) and turn away beginning Python programmers from writing tests
altogether.</p>

<p>Overall I do not feel that I have wasted my time reading <a
<a href="http://www.packtpub.com/python-testing-beginners-guide/book/mid/1503107sqs3w?utm_source=mg.pov.lt&utm_medium=affiliate&utm_content=blog&utm_campaign=mdb_002714">Python
  Testing</a>.  I look forward to reading <a
  href="http://python.genedrift.org/2010/03/03/python-testing-beginner’s-guide-review/">the</a>
<a
  href="http://www.blog.pythonlibrary.org/2010/03/06/book-review-python-testing/">other</a>
<a
  href="http://mcjeff.blogspot.com/2010/03/review-python-testing-beginners-guide.html">reviews</a>
that showed up on Planet Python.  I gathered that not all reviewers were happy
with the book, but avoided reading their reviews in order not to influence my
own.</p>

<p><strong>Update:</strong> I especially liked <a
  href="http://www.protocolostomy.com/2010/03/18/python-testing-beginners-guide-the-review/">this
  review by Brian Jones</a>.  The lack of awkward page breaks in code examples
is something that I only noticed after reading a different book, which is full
of such awkward breaks, <em>sigh</em>.</p>

<p><strong>Update 2:</strong> The book links are now affiliate links; I get a small
amount from any purchase you make through them. </p>
]]></content:encoded><dc:date>2010-03-13T19:54:48Z</dc:date></item><item><title>You've got to love profiling</title><guid isPermaLink="false">you-gotta-love-profiling</guid><link>http://mg.pov.lt/blog/you-gotta-love-profiling</link><description>Yesterday I slashed 50% of run time from our applications functional test suite by modifying a single function. I had ...</description><content:encoded><![CDATA[
<p>Yesterday I slashed 50% of run time from our applications functional test
suite by modifying a single function.  I had no idea that function was
responsible for 50% of the run time until I started profiling.</p>

<p>Profiling a Python program is getting easier and easier:</p>

<blockquote>
<pre>
<span class="prompt">$</span> <span class="typing">python -m cProfile -o prof.data bin/test -f</span>
</pre>
</blockquote>

<p>runs our test runner (which is a Python script) under the <a
  href="http://docs.python.org/library/profile.html">profiler</a> and stores
the results in prof.data.</p>

<blockquote>
<pre>
<span class="prompt">$</span> <span class="typing">runsnake prof.data</span>
</pre>
</blockquote>

<p>launches the <a
  href="http://www.vrplumber.com/programming/runsnakerun/">RunSnakeRun</a>
profile viewer, which displays the results visually:</p>

<p style="text-align: center">
<a href="http://mg.pov.lt/run-snake-run.png">
<img style="border: 1px solid #ccc; padding: 6px"
     src="http://mg.pov.lt/run-snake-run-square-map.png"
     alt="RunSnakeRun square map display" width="505" height="370" />
</a>
<br/>
The square map display of RunSnakeRun, with the 'render_restructured_text'
function highlighted.
</p>

<p>Who knew that ReStructuredText rendering could be such a time waster?  A
short caching decorator and the test suite is twice as fast.  The whole
exercise took me less than an hour.  I should've done it sooner.</p>

<p>Other neat tools:</p>

<ul>
  <li><a href="http://docs.python.org/library/profile.html?highlight=pstats#pstats.Stats">pstats</a>
  from the standard library lets you load and display profiler results from the
  command line (try <tt>python -m pstats prof.data</tt>).</li>
  <li><a href="http://pypi.python.org/pypi/pyprof2calltree">pyprof2calltree</a>
  converts Python profiler data files to a format that the popular profiler
  visualization tool <a
    href="http://kcachegrind.sourceforge.net/html/Home.html">kcachegrind</a>
  can understand.  It's somewhat less useful now that RunSnakeRun exists.</li>
  <li><a href="http://pypi.python.org/pypi/profilehooks">profilehooks</a> by
  yours truly has decorators for easily profiling individual functions instead
  of entire scripts.</li>
  <li><a href="http://pypi.python.org/pypi/keas.profile">keas.profile</a> and <a
    href="http://pypi.python.org/pypi/repoze.profile">repoze.profile</a> hook
  up the profiler as WSGI middleware for easy profiling of web apps.</li>
</ul>
]]></content:encoded><dc:date>2010-03-06T18:49:00Z</dc:date></item></channel></rss>