2007

Java BDD

I notice there’s another Behavior Driven Development framework for Java called Instinct (via). I have commented on BDD before.

Here’s an example test:

import static com.googlecode.instinct.expect.Expect.expect;
import com.googlecode.instinct.marker.annotate.BeforeSpecification;
import com.googlecode.instinct.marker.annotate.Context;
import com.googlecode.instinct.marker.annotate.Specification;

public final class AnEmptyStack {
    private Stack<Object> stack;

    @BeforeSpecification
    void setUp() {
        stack = new StackImpl<Object>();
    }

    @Specification
    void mustBeEmpty() {
        expect.that(stack.isEmpty()).equalTo(true);
    }
}

Yeah, that’s… great. What would it look like in a doctest?

>>> Stack<Object> stack = new StackImpl<Object>();
>>> stack.isEmpty()
true

Of course you have to invent a REPL for Java, but I’m sure that’s not very hard.

What does all that class infrastructure, setUp, mustBeEmpty and the weird DSLish stuff give you? Beats me. Doctest started in Python but now also exists in Ruby and Javascript. Someone needs to port the concept to Java too. People ported SUnit all over the place, so there’s no reason a good idea can’t spread. I can’t help feel that BDD is a case of a bad idea spreading; the motivations for BDD are fine (a change in developer testing workflow), but the technique they use to try to reach the desired workflow is totally bizarre.

Programming

Comments (20)

Permalink

Prism

I’ve seen talk of MS Silverlight and Adobe AIR. People talk them up like the future of web applications or something. I don’t know much about them, but I almost completely certain I don’t want anything to do with them.

Here’s a general rule I have: I don’t accept anything made by people who hate the web. If you hate the web and you want to improve the web, I don’t want anything to do with you. If you think the web is some kind of implementation detail then I probably don’t care what you are doing. If you still think the web is a fad, then you are just nuts; if all you can think of is reasons why the web is stupid and awkward, and you think it’s some giant step backward (from what?), then you haven’t thought very deeply about what’s happened in the world of technology and why.

To me Silverlight and AIR reek of a distaste for the web.

So it is with great delight that I read the announcement of Mozilla Prism: a bridge between the desktop and the web, written by people who don’t hate the web.

The premise of Prism from what I can tell is this: to make a web application into a desktop application, you just give the browser a shallowly separate identity. It lives in its own window, has its own icon, it’s own launcher. Maybe runs in its own process for reliability. You take away the chrome (URL bars, back button, etc). Unlike previous ideas for Mozilla (like xulrunner) you don’t add chrome back in with XUL, you just write all the controls with HTML and Javascript.

All the things that Prism isn’t is what makes it great, because it’s so damn simple. The only thing that really seems weird to me is that it is very separate from the browser itself; hopefully this is just temporary, and by the time it’s really "done" you’ll be able to just make any page an application directly from Firefox.

This doesn’t make web applications perfect, of course. There’s the offline issue. There’s usability issues, like keybindings. There’s lots of issues, but all of those issues apply just as much to the web as to a desktop application, and should be solved both places. Those things can be worked on orthogonally (most interestingly in Web Forms 2.0 and Web Applications 1.0 at WHAT-WG). Still, HTML and Javascript right now are totally workable for most applications.

Web
Javascript

Comments (17)

Permalink

Logo

So Logo is 40 years old. I’ll take this as an opportunity to talk about Logo-the-language (as opposed to Logo-the-graphics or Logo-the-educational-experience). It’s a much better language than most people appreciate.

Logo is Lisp. It’s an old Lisp but it’s very Lisp. Let’s look at a classic example:

TO square :size
  REPEAT 4 [FD :size RT 90]
END

This translates to something like this (in Scheme):

(define (square size)
  (repeat 4 (lambda ()
             (begin (fd size)
                    (rt 90)))))

Well, technically it translates to this:

(define (square size)
  (repeat 4 '(fd size rt 90)))

That is, […] is a quoted list; nothing inside it is evaluated. This is actually a lot like Tcl:

proc square {size} {
  repeat 4 {
    fd $size
    rt 90
  }
}

In Tcl {…} is just a string literal, but one without substitution, and where you can nest your braces. It’s a lot like a quoted list in Lisp, except where lists are the fundamental type in Lisp, strings are the fundamental type in Tcl.

To get an idea of how this works, here’s the implementation of repeat in these different languages:

TO repeat :count :block
  IF :count > 0 [EVAL :block
                 REPEAT (:count - 1) :block]
END

(define (repeat count block)
  (if (> count 0)
      (begin (block) (repeat (- count 1) block))))

proc repeat {count block} {
  while {$count > 0} {
      uplevel 1 $block
      set count [expr {$count - 1}]
  }
}

Tcl doesn’t really encourage recursion, and it’s only partially encouraged in Logo — some implementations implement tail call optimization, but not all of them.

Scoping is an interesting difference here. In Scheme the scoping is lexical, which is the norm in modern languages. That is, any variables you refer to in a lambda (like size in the example) are bound according to where in the source you define that anonymous function. In Tcl the uplevel statement says to evaluate the block in the calling scope. In Logo it’s all implicit because Logo is dynamically scoped. That is, each function call inherits all the variables from the calling function. So you can write something like this:

TO house :size
  square
  FD :size
  RT 60
  triangle
END

TO square
  REPEAT 4 [FD :size RT 90]
END

TO triangle
  REPEAT 3 [FD :size RT 120]
END

Here :size is inherited in called functions. You can write some really bad code using dynamic scoping, with lots of magic side effects, but Logo isn’t meant for writing large programs so it usually doesn’t come up. A nice side effect of dynamic scoping is that EVAL works pretty well.

Another interesting aspect of Logo (and Tcl) is that it has very few special forms. In one of Logo’s more pure implementations the only special form is TO, and even that wouldn’t have to be a special form (there’s a function DEFINE that does the same thing). For instance, here’s how you set a variable:

MAKE "variable <value>

That is, you call the function MAKE, give it a variable name, and then a value. Some dialects allow MAKE :variable <value>, but some like UCBLogo actually interpret that to mean: set the variable named by :variable.

Another interesting aspect of Logo is how it handles parenthesis. You’ll notice there aren’t many. Lisp is known (and oft criticized) for its parenthesis. Logo shows they aren’t strictly required; it
does this by using the arity of functions to inform parsing. An example:

SETXY ABS :x :y

The equivalent Scheme:

(setxy (abs x) y)

If you know that SETXY takes two arguments, and that ABS takes one argument, you can figure out how to parse the Logo. Comparing it to Forth is kind of interesting:

y @ x @ abs setyx

Reverse the order, replace @ with :, and you have Logo. @ and : also share a lot in common: :variable in Logo is syntactic sugar for THING "variable (symbols in Logo are spelled "symbol, like ’symbol in Scheme, #symbol in Smalltalk, or :symbol in Ruby). THING "variable tells Logo to look up the value associated with the given symbol. Similarly in Forth, y refers to an address, and @ says to lookup the value at that address.

But back to parsing - Forth is based on trust and foreknowledge. You know that abs pops one thing from the stack, and setyx pops two things from the stack. You trust those words to act that way, because there’s nothing stopping them from popping more or less from the stack than they claim. Logo isn’t quite as trusting, but it does see that SETXY takes two values, and grabs two values. In the same way in the first example, when it sees [FD :size RT 90], it can tell that it’s two commands — newlines aren’t required.

One awkward part of this lack of parenthesis is that without knowing what functions you are referring to the expressions can’t be trully parsed. As a result most Logos are really interpreters. If anyone cared enough of course you could optimize this considerably, and maybe some of the more performant Logos like StarLogo or NetLogo do, but I don’t really know. PyLogo is pretty naive about it, and it’s not that fast as a result (but not terrible).

And of course I should take a chance to plug PyLogo, which runs Logo from Python, and lets Logo code easily call Python, and vice versa. Just easy_install PyLogo and you can run pylogo –console to amuse yourself with the language (the docs from UCBLogo might be helpful, and PROCEDURES shows all the functions, while HELP "IFELSE will show the help for a particular command.

Another related language is Rebol, which is very close to Logo without the turtles (though with a bunch of new literals and object-oriented features as well). Many things called Logo are just turtle graphics with an extremely poor "language" bolted on in front. Don’t be fooled! On the web Turtle Tracks is one of the more true implementations (though I can’t get graphics, hm).

Programming

Comments (7)

Permalink

Workingenv is dead, long live Virtualenv!

A lot of people have found workingenv useful, but it’s always been a bit fragile. If you’ve seen the …/site.py is not a setuptools-generated site.py; please remove it. message, you know what I mean.

For a while I tried to refactor and improve workingenv, but it didn’t go very well. So I decided to ditch it completely and revisit the ideas of virtual-python.py. That script works by copying the Python executable, and in doing so change sys.prefix — it’s pretty consistent that all other paths in Python derive from sys.prefix.

The result is virtualenv, which I think is now featureful enough for general use. It might still be buggy, but it’s worked well for some of us, and I expect the bugs to all be much easier than they were in workingenv.

Unlike virtual-python.py, virtualenv works on Windows and in the latest release also Mac framework builds. It also handles site-packages better, so you can manage some of your packages using packages from your OS distribution (e.g., debs or rpms), while also installing environment-local packages.

So, in summary: use virtualenv, don’t use workingenv. And if you were using the –requirements option to workingenv, the package PoachEggs lets you do a similar batch installation after you’ve created the environment.

Python

Comments (28)

Permalink

lxml.html

Over the summer I did quite a bit of work on lxml.html. I’m pretty excited about it, because with just a little work HTML starts to be very usefully manipulatable. This isn’t how I’ve felt about HTML in the past, with all HTML emerging from templates and consumed only by browsers.

The ElementTree representation (which lxml copies) is a bit of a nuisance when representing HTML. A few methods improve it, but it is still awkward for content with mixed tags and text (common in HTML, uncommon in most other XML). Looking at Genshi Transforms there are some things I wish we could do, like simply "unwrap" text and then wrap it again. But once you remove a tag the text is thoroughly merged into its neighbors. Another little nuisance is that el.text and el.tail can be None, which means you have to guard a lot of code.

That said, here’s the Genshi example:

>>> html = HTML('''<html>
...   <head></head>
...   <body>
...     Some <em>body</em> text.
...   </body>
... </html>''')
>>> print html | Transformer('body/em').map(unicode.upper, TEXT) 
...                                    .unwrap().wrap(tag.u).end() 
...                                    .select('body/u') 
...                                    .prepend('underlined ')

Here’s how you’d do it with lxml.html:

>>> html = fromstring('''... same thing ...''')
>>> def transform(doc):
...     for el in doc.xpath('body/em'):
...         el.text = (el.text or '').upper()
...         el.tag = 'u'
...     for el in doc.xpath('body/u'):
...         el.text = 'underlined ' + (el.text or '')

I’m not sure if Genshi works in-place here, or makes a copy; otherwise these are pretty much equivalent. Which is better? Personally I prefer mine, and actually prefer it quite strongly, because it’s quite simple — it’s a function with loops and assignments. It’s practically pedestrian in comparison to the Genshi example, which uses methods to declaratively create a transformer.

Some of the things now in lxml.html include:

  • Link handling, which is particularly focused on rewriting links so you can put HTML fragments into a new context without breaking the relative links.

  • Smart doctest comparisons (attribute-order-neutral comparisons, with improved diffs, and also whitespace neutral, based loosely on formencode.doctest_xml_compare). Inside your doctest choose XML parsing with from lxml import   usedoctest or HTML parsing with from lxml.html import   usedoctest. I consider the import trick My Worst Monkeypatch Ever, but it kind of reads nicely. For testing it is very nice.

  • Cleaning code, to avoid XSS attacks, in lxml.html.clean. This is still pretty messy, because there’s lots of little things you may or may not want to protect against. E.g., I think I can mostly clean out style tags (at least of Javascript), but some people might want to remove all style. So there’s an option. There’s lots of options. Too many.

  • With the cleaning code there’s word-wrapping code and autolinking code. I think of these as clean-up-people’s-scrappy-HTML tools. Also important for putting untrusted HTML in a new context.

  • I rewrote htmlfill in lxml.html.formfill. It’s a bit simpler, and keeps error messages separate from actual value filling. They were really only combined because I didn’t want to do two passes with HTMLParser for the two steps, but that doesn’t matter when you load the document into memory. I also stopped using markup like <form:error> for placing error messages; it’s all automatic now, which I suppose is both good and bad.

  • After I wrote lxml.html.formfill I got it into my head to make smarter forms more natively. So now you can do:

    >>> from lxml.html import parse
    >>> page = parse('http://tripsweb.rtachicago.com/').getroot()
    >>> form = page.forms[0]
    >>> from pprint import pprint
    >>> pprint(form.form_values())
    [('action', 'entry'),
     ('resptype', 'U'),
     ('Arr', 'D'),
     ('f_month', '09'),
     ('f_day', '21'),
     ('f_year', '2007'),
     ('f_hours', '9'),
     ('f_minutes', '30'),
     ('f_ampm', 'AM'),
     ('Atr', 'N'),
     ('walk', '0.9999'),
     ('Min', 'T'),
     ('mode', 'A')]
    >>> for key in sorted(f.fields.keys()):
    ...     print key
    None
    Arr
    Atr
    Dest
    Min
    Orig
    action
    dCity
    endpoint
    f_ampm
    f_day
    f_hours
    f_minutes
    f_month
    f_year
    mode
    oCity
    resptype
    startpoint
    walk
    >>> f.fields['Orig'] = '1500 W Leland'
    >>> f.fields['Dest'] = 'LINCOLN PARK ZOO'
    >>> from lxml.html import submit_form()
    >>> result = parse(submit_form(f)).getroot()
    

    From there I’d have to actually scrape the results to figure out what the best trip was, which isn’t as easy.

  • HTML diffing and something like svn blame for a series of documents, in lxml.html.diff. Someone noted a similarity between htmldiff and templatemaker, and they are conceptually similar, but with very different purposes. htmldiff goes to great trouble to ignore markup and focus only on changes to textual content. As such it is great for a history page. templatemaker focuses on the dissection of computer-generated HTML and extracting its human-generated components. Templatemaker is focused on screen scraping. It might be handy in that form example above…

  • There’s also a fairly complete implementation of CSS 3 selectors. It would be interesting to mix this with cssutils.

    Though some people aren’t so enthusiastic about CSS namespaces (and I can’t really blame him), conveniently this CSS 3 feature makes CSS selectors applicable to all XML. I don’t know if anyone is actually going to use them instead of XPath on non-HTML documents, but you could. Because the implementation just compiles CSS to XPath, you could potentially use this module with other XML libraries that know XPath. Of which I only actually know one (or two <http://genshi.edgewall.org/>?) — though compiling CSS to XPath, then having XPath parsed and interpreted in Python, is probably not a good idea. But if you are so inclined, there’s also a parser in there you could use.

  • lxml and BeautifulSoup are no longer exclusive choices: lxml.html.ElementSoup.parse() can parse pages with BeautifulSoup into lxml data structures. While the native lxml/libxml2 HTML parser works on pretty bad HTML, BeautifulSoup works on really bad HTML. It would be nice to have something similar with html5lib.

HTML
Python

Comments (7)

Permalink

2 Python Environment Experiments

two experiments in the Python environment. The first is virtualenv, which is a rethinking of virtual-python.py, and my attempt to move away from workingenv. It works mostly like virtual-python.py, and on systems where it works (not Windows, nor Framework Mac Python) I think it works considerably better than workingenv. No more not a setuptools’ site.py, in particular. The basic technique is that it creates/copies a new python executable, and anything that uses that executable (including a script that references that executable with #!) will use that environment.

On the systems where it doesn’t work, I’m not quite sure what to do. The problem with the Mac is that sys.prefix is not determined by the location of the python executable, it’s hard-coded in some fashion. I asked about it on distutils-sig and got some response, but haven’t figured out any solution yet.

On Windows similarly sys.prefix is not determined by the executable location. What it’s determined by there I don’t know — the location of python25.dll, something in the registry? If I could figure it out, perhaps this could work there too — the existance of symlinks isn’t as important as it was with virtual-python.py.

If I can get these figured out, I think this will be a much happier experience than workingenv, and a somewhat friendlier experience than virtual-python.py. On non-Mac posix systems it works well right now.

The other experiment is in buildutils (downloadable with Mercurial): a new command python setup.py bundle, run in the application package you want to bundle. This creates a directory with all the dependencies of the application, and scripts that load up the appropriate dependencies. You can then ship the entire thing in a zip file as a runnable application that doesn’t require any installation except for unpacking.

Actually creating the bundle can be a little finicky, because easy_install has a tendency to prefer things on the local machine even though it shouldn’t. Probably it would be best to run this inside a virtualenv; when you are done you can also feel more confident that you’ve actually included all the dependencies (if you use –no-site-packages when creating the virtualenv).

Anyway, while both of these are a little incomplete I’m feeling optimistic about them, and I’m hoping intrepid souls can give feedback on how they work.

Update: virtualenv 0.8.2 is out, featuring Better Error Messages (and nothing else). Still doesn’t work on Mac Framework Pythons, or Windows. You’ll have to keep using workingenv there — but patches extremely welcome! Contact me if you are interested in supporting these platforms. It will involve some digging, but maybe we can just do the digging once for everyone.

Update 2: virtualenv 0.8.3 is out, featuring Windows!

Python
Programming

Comments (10)

Permalink

FlatAtompub

A little while ago I decided to whip up a small Atompub server to get my head around the Atom Publishing Protocol. I called it FlatAtomPub because it was just storing stuff in flat files. I’m not committing to that name. It was also a chance to kick the tires on WebOb.

What I take out of the process:

  • APE was very handy. I lazily did very little unit testing, instead relying on APE to do the work for me. This seemed to work quite well. It is fun doing test driven development when someone else develops the tests.
  • I wrote a little decorator that serves as a kind of framework. It worked pretty well, I think. This might be the prototype of what the PylonsController.__call__ method does in some WebOb-using future.
  • Stuff like conditional requests and responses are mostly implemented in WebOb itself, which works well. HTTP is clear about how conditional requests work, so if you can just setup the basic info (ETag, Last-Modified) you can let the library handle the rest. I could probably save a little work by paying closer attention to ETags and Last-Modified up-front, but since there’s no complicated template rendering the work saved doesn’t seem significant.
  • The atom library removed most concern about the XML itself.
  • I don’t know what to with collections. I guess I could allow multiple collections via configuration. If the store wasn’t a dumb store (e.g., it was plugged directly into a blog, it didn’t just passively store things) it would be clearer what a collection would mean. As it is, collections are just a way of aggregating multiple Atompub servers into one service document, which doesn’t seem very useful.
  • Handling links and slugs is kind of annoying. I took the lazy way out for this, using relative links and treating the slug and link as the same thing. This isn’t a good long-term solution, as it can mess things up if you start handing entries around, or worse move a server, and I don’t even set xml:base on elements. In theory it should all work, but it makes the client do more effort than I would like. On the other hand, I suppose a client should do that extra work anyway, as it shouldn’t rely on the server not being lazy. So maybe I’m better off sticking with a lazy solution, and making sure I work with non-lazy clients.
  • I considered pluggable storage, but ultimately decided it didn’t matter. Storing entries in files is fine; files are easy, and they work. I put in pluggable indexing instead. Amplee is another Python Atompub server, and I looked at Amplee storage backends. It’s kind of clever to have things like svn or S3 backends. But I’m not sure what use I’d actually do with that.
  • I haven’t yet considered transactions; if something fails part way through, stuff will be inconsistent. Admittedly this is where files make things harder. Probably a clear way to re-index would be useful too, as at least there’s a clear location for the canonical data (the files).
  • The dependencies are still a little tangled; even though the library doesn’t use a great deal of stuff, there’s enough pieces that some people have had a hard time getting it setup.
  • Ignoring authentication is nice. I should see what it takes to setup some authentication, but implementing it directly is out of scope for FlatAtomPub.

Interested people can look at the svn repository.

This makes me wonder how hard WebDAV would be…

Web
Python
Programming

Comments (4)

Permalink

Re-raising Exceptions

After reading Chris McDonough’s What Not To Do When Writing Python Software, it occurred to me that many people don’t actually know how to properly re-raise exceptions. So a little mini-tutorial for Python programmers, about exceptions…

First, this is bad:

try:
    some_code()
except:
    revert_stuff()
    raise Exception("some_code failed!")

It is bad because all the information about how some_code() failed is lost. The traceback, the error message itself. Maybe it was an expected error, maybe it wasn’t.

Here’s a modest improvement (but still not very good):

try:
    some_code()
except:
    import traceback
    traceback.print_exc()
    revert_stuff()
    raise Exception("some_code failed!")

traceback.print_exc() prints the original traceback to stderr. Sometimes that’s the best you can do, because you really want to recover from an unexpected error. But if you aren’t recovering, this is what you should do:

try:
    some_code()
except:
    revert_stuff()
    raise

Using raise with no arguments re-raises the last exception. Sometimes people give a blank never use “except:“ statement, but this particular form (except: + raise) is okay.

There’s another form of raise that not many people know about, but can also be handy. Like raise with no arguments, it can be used to keep the traceback:

try:
    some_code()
except:
    import sys
    exc_info = sys.exc_info()
    maybe_raise(exc_info)

def maybe_raise(exc_info):
    if for some reason this seems like it should be raised:
        raise exc_info[0], exc_info[1], exc_info[2]

This can be handy if you need to handle the exception in some different part of the code from where the exception happened. But usually it’s not that handy; it’s an obscure feature for a reason.

Another case when people often clobber the traceback is when they want to add information to it, e.g.:

for lineno, line in enumerate(file):
    try:
        process_line(line)
    except Exception, exc:
        raise Exception("Error in line %s: %s" % (lineno, exc))

You keep the error message here, but lose the traceback. There’s a couple ways to keep that traceback. One I sometimes use is to retain the exception, but change the message:

except Exception, exc:
    args = exc.args
    if not args:
        arg0 = ''
    else:
        arg0 = args[0]
    arg0 += ' at line %s' % lineno
    exc.args = arg0 + args[1:]
    raise

It’s a little awkward. Technically (though it’s deprecated) you can raise anything as an exception. If you use except Exception: you won’t catch things like string exceptions or other weird types. It’s up to you to decide if you care about these cases; I generally ignore them. It’s also possible that an exception won’t have .args, or the string message for the exception won’t be derived from those arguments, or that it will be formatted in a funny way
(KeyError formats its message differently, for instance). So this isn’t foolproof. To be a bit more robust, you can get the exception like this:

except:
    exc_class, exc, tb = sys.exc_info()

exc_class will be a string, if someone does something like raise "not found". There’s a reason why that style is deprecated. Anyway, if you really want to mess around with things, you can then do:

new_exc = Exception("Error in line %s: %s"
                    % (lineno, exc or exc_class))
raise new_exc.__class__, new_exc, tb

The confusing part is that you’ve changed the exception class around, but you have at least kept the traceback intact. It can look a little odd to see raise ValueError(…) in the traceback, and Exception in the error message.

Anyway, a quick summary of proper ways to re-raise exceptions in Python. May your tracebacks prosper!

Update: Kumar notes the problem of errors in your error handler. Things get more long winded, but here’s the simplest way I know of to deal with that:

try:
    code()
except:
    exc_info = sys.exc_info()
    try:
        revert_stuff()
    except:
        # If this happens, it clobbers exc_info, which is why we had
        # to save it above
        import traceback
        print >> sys.stderr, "Error in revert_stuff():"
        traceback.print_exc()
    raise exc_info[0], exc_info[1], exc_info[2]

Python

Comments (18)

Permalink

9/11/2007

So, today is 9/11. I almost missed it. It’s not like it catches you by surprise, you’re not going to forget the date. But it’s just been slipping by for a few years now without much notice.

As an event it is still very important. History flowed from that day. But it doesn’t mean anything anymore.

Remember how everyone was saying, on those days after 9/11/2001, that they thought about life differently, about the things that really mattered and the things that didn’t? A couple years ago I felt frustrated by how quickly that seemed to disappear, how quickly genuine sentiment turned into empty rhetoric. A few years ago that transition was frustrating, now the whole thing seems laughable. The death of irony? No… after 9/11 our modern cynicism was down but it wasn’t out. It came back fighting, and a National Sense Of Grief was no match.

Whatever. I’m tired of it anyway. You win Whatever, you’re the champ.

Non-technical
Politics

Comments (9)

Permalink

Doctest for Ruby

Finally, someone wrote a version of doctest for Ruby.

Recently I’ve been writing most of my tests using stand-alone doctest files. It’s a great way to do TDD — mostly because the cognitive load is so low. Also, I write my examples but don’t write my output, then copy the output after visually confirming it is correct. So the basic pattern is:

  • Figure out what I want to do
  • Figure out how I want to test it
  • Automate my conditions
  • Manually inspect whether the output is correct (i.e., implement and debug)
  • Copy the output so that in the future the manual process is automated (doctest-mode for Emacs makes this particularly easy)

The result is a really good balance of manual and automated testing, I think giving you the benefit of both processes — the ease of manual testing, and the robustness of automated testing.

Another good thing about doctest is it doesn’t let you hide any boilerplate and setup. If it’s easy to use doctest, it’s probably easy to use the library.

There’s nothing Python-specific about doctest (e.g., doctestjs), so it’s good to see it moving to other languages. Even if the language doesn’t have a REPL, IMHO it’s worth inventing it just for this.

Ruby
Javascript
Python
Programming

Comments (2)

Permalink